理解 JVM 性能 –它很快 –非常快!






4.64/5 (8投票s)
JVM 代码可以和等效的本地代码一样快:为什么以及如何实现。
场景
这通常是真的。然而,在这种情况下,我正在关注批处理,这是一种在商业环境中非常常见的编程类型。在这种编程模型中,许多离散的数据处理程序按顺序运行,传统上由某种脚本控制。在大型机上,该脚本通常是 JCL(作业控制语言);在分布式计算机上,它要么是 shell 脚本,要么是 cmd(.bat)脚本。
分布式(非大型机)硬件上传统批处理的性质意味着大量短时运行的进程被利用。这正是 JVM 程序不擅长的。然而,通过良好的批处理系统架构,这一点可以克服,并取得惊人的效果。
要证明 JVM 程序可以有多快,需要克服一系列挑战。
挑战
证明这一点**的第一个挑战**是缺乏直接可比的语言。使用 gcj 将 Java 编译为本地代码可以工作;然而,即使结果通常比在 JVM 上运行相同的 Java 字节码慢,但这并不能证明什么,因为 gcj 编译器不是一个高度信任、高度优化的商业级编译器。并非冒犯,它从未以这种方式开发过。
作为 Micro Focus 的 JVM COBOL 团队的高级首席开发人员,我几乎处于一个独特的位置,可以将我们久经考验的本地 COBOL 编译器与我们即将全面上市的 JVM COBOL 编译器进行比较。编译器前端是相同的! 唯一的区别在于代码生成器。本地编译器拥有一个极其有效的优化本地代码(机器码)生成器,而 JVM 编译器直接生成类文件格式的 JVM 字节码。
注意:Micro Focus JVM COBOL 编译器直接生成字节码,不经过中间步骤。
第二个挑战来自于 JVM 代码的运行方式。我在 Tuning the JVM for Unusual Uses - Have Some Tricks Under Your Hat 中详细解释了这一点。该文章解释了 JVM 执行的解释/分析、编译和本地阶段。
因此,我们需要一种方法来反复运行 COBOL 程序,或者运行许多单独的 COBOL 程序,使用同一个 JVM 进程。
第三个挑战是如何重现运行批处理文件的所有优点,同时又能高效地利用基于 JVM 的 COBOL?
方法
JavaScript 拯救了我们。JavaScript 是一种令人惊叹的简单而强大的编程语言。有一个纯 JVM 实现,名为 Rhino。Rhino 是开源的,由 Firefox 的开发人员——Mozilla 开发。
你可以用 Rhino 和 JVM COBOL 做一些令人惊叹的事情。我将在不久的将来写一篇完整的文章来介绍这一点。然而,关键在于我们可以直接从脚本调用 JVM COBOL 程序。例如,要运行程序 cobol_2(参见本帖底部源代码),在 JavaScript 中只需要 Packages.cobol.cobol_2.main(null);
,是的,就这么简单!这一行会在 JVM 命名空间(Java 人称之为包)中查找程序。要将其放入其中,我只需要使用 Micro Focus 编译器像这样编译它
cobol cobol_2.cbl jvmgen noanim ilnamespace(cobol);
我可以通过这样做将完全相同的代码编译为本地代码
cobol cobol_2.cbl opt;
cbllink cobol_2
这样,我就创建了相同的 JVM 和本地 COBOL 程序。要从 JavaScript 执行本地版本,需要 runtime.exec("cobol_2")).waitFor();
。有关更多详细信息,请查看下面的 JavaScript 源代码。
JavaScript 的力量
JavaScript 方法的强大之处在于脚本在更改后不需要重新编译。将编译后的程序封装在一个解释型脚本语言中是一种非常快速的开发方式。我第一次这样做是在将 FORTRAN 量子力学代码封装在一个名为 TCL 的脚本语言中时。
JavaScript 是无处不在的(你可能正在这个网页上运行一些),面向对象的,快速的,易于使用的,而且功能强大。它非常适合运行像 COBOL 这样高性能语言的大型操作的批处理控制逻辑。
这个项目用 JavaScript 来做比用任何编译型语言来做都**更快更容易**。
你可以将这视为“乐高”式编程。强大的 COBOL 模块可以被 JavaScript 以任何顺序排列,以创建许多不同且有用的系统,而无需触碰 COBOL 本身。
关键在于 JavaScript 可以在**同一个 JVM 中运行 JVM COBOL 程序以及脚本本身**。这是因为 JVM COBOL 程序与 Java 类一样,完全符合 JVM 标准。
使测试更具现实性
请看下面的 cobol_2 和 cobol_3。第一个是纯数学处理程序。第二个创建了一个包含 10,000 条索引记录的文件,然后按索引读取它们。后者程序真正突显了 COBOL 批处理为何仍然如此受欢迎和至关重要。即使在强大的关系数据库中,创建和按索引读取 10,000 条记录也需要一些时间。在 COBOL 中,在笔记本电脑上只需要几秒钟!
JVM COBOL 和本地执行之间的一个区别是进程启动开销;这是操作系统启动本地 COBOL 进程所需的时间。为了衡量这一点,我们使用了一个仅包含单个 goback
语句的 COBOL 程序的基准测试 JavaScript。
我使用 Visual COBOL 1.4 的开发中代码进行了测试。今年晚些时候发布的通用版本代码的性能应该非常相似。我在安装了 Windows 和 Enterprise 64 位版本的 Dell E6400 笔记本电脑上以 32 位模式运行了代码。
结果
为了确保公平性,我运行了三次测试。每次测试都对 JVM COBOL 中的两个程序各运行 32 次,对本地 COBOL 中的两个程序也各运行 32 次。使用 JavaScript Date
对象以毫秒为单位测量了每个程序的执行时间。记录并报告了最大、平均、最小和总执行时间。
运行 1
在此次运行中,JVM 方法总体上比本地方法稍快。
Results:
=========
JVM
Maximum Time: 1300
Minimum Time: 381
Mean Time: 624.3125
Total Time: 19978
Native
Maximum Time: 1404
Minimum Time: 525
Mean Time: 661.1875
Total Time: 21158
运行 2
同样,在此次运行中,JVM 方法总体上比本地方法稍快。
Results:
=========
JVM
Maximum Time: 1283
Minimum Time: 385
Mean Time: 670.53125
Total Time: 21457
Native
Maximum Time: 1559
Minimum Time: 526
Mean Time: 705.03125
Total Time: 22561
运行 3
在这里,我们看到本地方法略微领先于 JVM 方法。真的,两者之间没有区别。
Results:
=========
JVM
Maximum Time: 1320
Minimum Time: 390
Mean Time: 734.25
Total Time: 23496
Native
Maximum Time: 1782
Minimum Time: 528
Mean Time: 698.75
Total Time: 22360
CPU 密集型:我进行了一次运行,其中没有调用文件处理程序。这意味着唯一运行的代码是数学代码。这里的结果惊人地有利于 JVM 方法。如果我根据第一次迭代来选择原生和 JVM,我会认为原生更快。基于 32 次迭代的组,JVM 被证明是原生的两倍快。然而,如果算上进程启动开销,这也会产生误导。
Results:
=========
JVM
Maximum Time: 354
Minimum Time: 59
Mean Time: 101.09375
Total Time: 3235
Native
Maximum Time: 272
Minimum Time: 169
Mean Time: 203.53125
Total Time: 6513
First iteration:
JVM = 354
Native= 272
进程启动开销
Results:
=========
JVM
Maximum Time: 313
Minimum Time: 0
Mean Time: 9.84375
Total Time: 315
Native
Maximum Time: 72
Minimum Time: 54
Mean Time: 57.84375
Total Time: 1851
在 32 次进程启动周期中,进程启动开销约为 1.5 秒。这不足以在质上改变上述任何结果。例如,如果我们从 CPU 密集型测试的 6.5 秒时间中减去 1.5 秒,JVM COBOL 仍然比本地快 1.8 秒(55%)。
结论
- 在 JVM 和本地 COBOL 中运行一个小型程序,并不能作为真实世界性能的基准。
- 从 JavaScript 运行 JVM COBOL 是实现批处理的一种令人咋舌的快速简便的方法。
- 在典型的批处理应用程序中,JVM 管理的 COBOL 在 32 位 Intel 架构上的性能与本地 Micro Focus COBOL 相当(其他平台未测试)。
- 进程启动开销很大。这种方法比传统的批处理更好,因为它克服了进程启动开销。然而,即使在考虑了进程启动开销之后,在本次测试的范围内,JVM COBOL 的性能仍然不比本地差。
附录
function bench()
{
this.min = 10000000;
this.max = 0;
this.count = 0;
this.current = 0;
this.total = 0;
this.update = function()
{
if(this.current > this.max)
{
this.max = this.current;
}
if(this.current < this.min)
{
this.min = this.current;
}
++this.count;
this.total += this.current;
}
this.mean = function()
{
return this.total / this.count;
}
this.display = function()
{
display(" Maximum Time: " + this.max);
display(" Minimum Time: " + this.min);
display(" Mean Time: " + this.mean());
display(" Total Time: " + this.total);
}
}
var jvm = new bench();
var nat = new bench();
var its = 32;
var runtime=java.lang.Runtime.getRuntime();
display("JVM COBOL Benchmark");
for(var i=0;i<its;++i)
{
var start = (new Date()).getTime();
Packages.cobol.cobol_2.main(null);
Packages.cobol.cobol_4.main(null);
jvm.current = ((new Date()).getTime())-start;
display("" + jvm.current);
jvm.update();
}
display("Native COBOL Benchmark");
for(var i=0;i<its;++i)
{
var start = (new Date()).getTime();
(runtime.exec("cobol_2")).waitFor();
(runtime.exec("cobol_3")).waitFor();
nat.current = ((new Date()).getTime())-start;
display("" + nat.current);
nat.update();
}
display("");
display("Results: ");
display("=========");
display("JVM");
jvm.display();
display("Native");
nat.display();
function display(what)
{
java.lang.System.out.println(what);
}
123456$set sourceformat(variable)
01 my-group.
03 counter pic s9(9) comp-5.
03 a pic s9(9) comp-5.
03 b pic s9(9) comp-5.
03 r pic s9(9) comp-5.
move 123456789 to a b r
perform varying counter from 1 by 1 until counter = 1000000
compute r = (a + b) / (a - b)
compute r = (r + b) / (a - b)
compute r = (r + b) / (a - b)
compute r = (r + b) / (a - b)
compute r = (r + b) / (a - b)
end-perform
.
123456$set sourceformat(variable)
input-output section.
file-control.
select source-file
assign to disk "count.idx"
organization indexed
access dynamic
record key is r-key
status is source-status.
data division.
file section.
fd source-file.
01 source-record.
03 raw-line pic x(256).
03 source-line redefines raw-line.
05 filler pic x(7).
05 r-key pic x(10).
05 source-body pic x(249).
working-storage section.
01 counter binary-long.
01 check binary-long.
01 source-status pic 99.
procedure division.
open output source-file
perform varying counter from 1 by 1 until counter = 10000
move counter to source-body r-key
write source-record
end-perform
close source-file
open input source-file
perform varying counter from 1 by 1 until counter = 10000
move counter to r-key
read source-file
move source-body(1:9) to check
if check not = counter
display "woops"
end-if
end-perform
close source-file
启动 JavaScript
为了方便启动 JavaScript,我创建了一个批处理文件,其中包含此行
"\Program Files (x86)\Java\jdk1.6.0_21\bin\java"
-server org.mozilla.javascript.tools.shell.Main %1 %2 %3 %4 %5
最后说明:我在这篇文章中已尽力进行同类比较。在数学 COBOL 中,我比较了 comp-5 组项的使用。对于本地和 JVM COBOL,这意味着对于每个计算,程序都应该从分配给工作存储的内存块加载和存储计算值。如果 COBOL 更改为这样(删除 my-group 标签)……
01.
03 counter pic s9(9) comp-5.
03 a pic s9(9) comp-5.
03 b pic s9(9) comp-5.
03 r pic s9(9) comp-5.
……编译器可以以一种更有效的方式处理 JVM COBOL 中的工作存储项。以这种方式进行的计算结果是
Results:
=========
JVM
Maximum Time: 342
Minimum Time: 0
Mean Time: 11
Total Time: 352
Native
Maximum Time: 223
Minimum Time: 183
Mean Time: 195.09375
Total Time: 6243
是的,这里的 JVM COBOL 运行速度几乎是本地的 20 倍。然而,这是一种有些人为的情况,因此我没有将其包含在本文使用的结果中。同样,考虑到进程启动开销只会使这种差异在质上减少到 13.5 倍;JVM COBOL 仍然非常快。