大方法的执行性能与调优过程小记

该文章转载自:http://rdc.taobao.com/team/jm/archives/552

 

你写过超过2500行的方法么?通常来说,这么大的方法并不多见,一般都是使用机器辅助生成的为主,这种情况在模板编译或其它语言的自动转换中比较常见。例如,对一个复杂的JSP页面,机器有可能会为它生成一个复杂的servlet方法去实现。

然而在Hotspot上运行这种大方法,很可能会有性能问题。例如,把文章所附DEMO的play()方法的内容分别重复拷贝1、2、4、8、16、32次并依次运行,在我的机器(Hotspot_1.6u22/Windows)上得到的play()的执行消耗时间分别是28.43、54.72、106.28、214.41、419.30、1476.40毫秒/万次。在重复拷贝1~16次时,随着代码量增加,方法执行所消耗的时间也对应成倍增加。当重复拷贝32次时,方法却多消耗了80%的时间。如果把这个play()方法拆分成play1()和play2(),让它们的方法体都是16次的重复拷贝,play1()最后再调用play2(),那么,play1()+play2()的执行消耗时间是857.75毫秒/万次,恰好是之前重复拷贝16次所消耗的时间的两倍。为什么同样功能的一段代码放在一个方法中执行会变慢,拆分成两个方法就变快?

大家知道,JVM一开始是以解释方式执行字节码的。当这段代码被执行的次数足够多以后,它会被动态优化并编译成机器码执行,执行速度会大大加快,这就是所谓的JIT编译。DEMO的play()方法在被统计消耗时间之前,已经预热执行了2000次,满足ClientVM的方法JIT编译阈值CompileThreshold=1500次的要求,那么,它是不是真的被JIT编译了呢?我们可以增加VM参数”-XX:+PrintCompilation”调查一下。在+PrintCompilation打开以后,列出了JVM在运行时进行过JIT编译的方法。下面是经过32次重复拷贝的play()方法的JIT编译记录(只列出需要关心的部分):

34       HugeMethodDemo::buildTheWorld (184 bytes)
39       HugeMethodDemo::run (59 bytes)

而分成两部分的play1()+plaay2()的JIT编译记录则为:

18       HugeMethodDemo::play1 (4999 bytes)
19       HugeMethodDemo::play2 (4993 bytes)
36       HugeMethodDemo::buildTheWorld (184 bytes)
41       HugeMethodDemo::run (59 bytes)

显然,经过重复拷贝32次的play()方法没有经过JIT编译,始终采用解释方式执行,而分拆开的play1()+play2()经过JIT编译,所以难怪play()要慢80%。

为什么play()方法不受JVM青睐呢,是太长了么?这只能到Hotspot源码中去翻答案了。在compilationPolicy.cpp中有写道:

// Returns true if m is allowed to be compiled
bool CompilationPolicy::canBeCompiled(methodHandle m) {
 if (m->is_abstract()) return false;
 if (DontCompileHugeMethods && m->code_size() > HugeMethodLimit) return false;
 // Math intrinsics should never be compiled as this can lead to
 // monotonicity problems because the interpreter will prefer the
 // compiled code to the intrinsic version.  This can't happen in
 // production because the invocation counter can't be incremented
 // but we shouldn't expose the system to this problem in testing
 // modes.
 if (!AbstractInterpreter::can_be_compiled(m)) {
 return false;
 }
 return !m->is_not_compilable();
}

当DontCompileHugeMethods=true且代码长度大于HugeMethodLimit时,方法不会被编译。DontCompileHugeMethods与HugeMethodLimit的值在globals.hpp中定义:

product(bool, DontCompileHugeMethods, true,
        "don't compile methods > HugeMethodLimit")
develop(intx, HugeMethodLimit,  8000,
        "don't compile methods larger than this if +DontCompileHugeMethods")

上面两个参数说明了Hotspot对字节码超过8000字节的大方法有JIT编译限制,这就是play()杯具的原因。由于使用的是product mode的JRE,我们只能尝试关闭DontCompileHugeMethods,即增加VM参数”-XX:-DontCompileHugeMethods”来强迫JVM编译play()。再次对play()进行测试,耗时855毫秒/万次,性能终于上来了,输出的JIT编译记录也增加了一行:

16       HugeMethodDemo::play (9985 bytes)

使用”-XX:-DontCompileHugeMethods”解除大方法的编译限制,一个比较明显的缺点是JVM会尝试编译所遇到的所有大方法,者会使JIT编译任务负担更重,而且需要占用更多的Code Cache区域去保存编译后的代码。但是优点是编译后可以让大方法的执行速度变快,且可能提高GC速度。运行时Code Cache的使用量可以通过JMX或者JConsole获得,Code Cache的大小在globals.hpp中定义:

define_pd_global(intx, ReservedCodeCacheSize, 48*M);
product_pd(uintx, InitialCodeCacheSize, "Initial code cache size (in bytes)")
product_pd(uintx, ReservedCodeCacheSize, "Reserved code cache size (in bytes) - maximum code cache size")
product(uintx, CodeCacheMinimumFreeSpace, 500*K, "When less than X space left, we stop compiling.")

一旦Code Cache满了,HotSpot会停止所有后续的编译任务,虽然已编译的代码不受影响,但是后面的所有方法都会强制停留在纯解释模式。因此,如非必要,应该尽量避免生成大方法;如果解除了大方法的编译限制,则要留意配置Code Cache区的大小,准备更多空间存放编译后的代码。

最后附上DEMO代码:

import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
  
public class HugeMethodDemo {
  
 public static void main(String[] args) throws Exception {
   HugeMethodDemo demo = new HugeMethodDemo();
  
   int warmup = 2000;
   demo.run(warmup);
  
   int loop = 200000;
   double total = demo.run(loop);
   double avg = total / loop / 1e6 * 1e4;
  
   System.out.println(String.format(
     "Loop=%d次, " + "avg=%.2f毫秒/万次", loop, avg));
 }
  
 private long run(int loop) throws Exception {
   long total = 0L;
  
   for (int i = 0; i < loop; i++) {
     Map theWorld = buildTheWorld();
     StringWriter console = new StringWriter();
  
     long start = System.nanoTime();
     play(theWorld, console);
     long end = System.nanoTime();
     total += (end - start);
   }
  
   return total;
 }
  
 private Map buildTheWorld() {
   Map context = new HashMap();
   context.put("name", "D&D");
   context.put("version", "1.0");
  
   Map game = new HashMap();
   context.put("game", game);
  
   Map player = new HashMap();
   game.put("player", player);
   player.put("level", "26");
   player.put("name", "jifeng");
   player.put("job", "paladin");
   player.put("address", "heaven");
   player.put("weapon", "sword");
   player.put("hp", 150);
  
   String[] bag = new String[] { "world_map", "dagger",
     "magic_1", "potion_1", "postion_2", "key" };
   player.put("bag", bag);
   return context;
 }
  
 private void play(Map theWorld, Writer console) throws Exception {
   // 重复拷贝的开始位置
   if (true) {
     String name = String.valueOf(theWorld.get("name"));
     String version = String.valueOf(theWorld.get("version"));
     console.append("Game ").append(name).append(" (v").append(version).append(")\n");
     Map game = (Map) theWorld.get("game");
     if (game != null) {
       Map player = (Map) game.get("player");
       if (player != null) {
         String level = String.valueOf(player.get("level"));
         String job = String.valueOf(player.get("job"));
         String address = String.valueOf(player.get("address"));
         String weapon = String.valueOf(player.get("weapon"));
         String hp = String.valueOf(player.get("hp"));
         console.append("  You are a ").append(level).append(" level ").append(job)
                .append(" from ").append(address).append(". \n");
         console.append("  Currently you have a ").append(weapon).append(" in hand, ")
                .append("your hp: ").append(hp).append(". \n");
         console.append("  Here are items in your bag: \n");
         for (String item : (String[]) player.get("bag")) {
           console.append("     * ").append(item).append("\n");
         }
       } else {
         console.append("\tPlayer not login.\n");
       }
     } else {
       console.append("\tGame not start yet.\n");
     }
   }
   // 重复拷贝的结束位置
 }
}
[/java]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
水资源是人类社会的宝贵财富,在生活、工农业生产中是不可缺少的。随着世界人口的增长及工农业生产的发展,需水量也在日益增长,水已经变得比以往任何时候都要珍贵。但是,由于人类的生产和生活,导致水体的污染,水质恶化,使有限的水资源更加紧张。长期以来,油类物质(石油类物质和动植物油)一直是水和土壤中的重要污染源。它不仅对人的身体健康带来极大危害,而且使水质恶化,严重破坏水体生态平衡。因此各国都加强了油类物质对水体和土壤的污染的治理。对于水中油含量的检测,我国处于落后阶段,与国际先进水平存在差距,所以难以满足当今技术水平的要求。为了取得具有代表性的正确数据,使分析数据具有与现代测试技术水平相应的准确性和先进性,不断提高分析成果的可比性和应用效果,检测的方法和仪器是非常重要的。只有保证了这两方面才能保证快速和准确地测量出水中油类污染物含量,以达到保护和治理水污染的目的。开展水中油污染检测方法、技术和检测设备的研究,是提高水污染检测的一条重要措施。通过本课题的研究,探索出一套适合我国国情的水质污染现场检测技术和检测设备,具有广泛的应用前景和科学研究价值。 本课题针对我国水体的油污染,探索一套检测油污染的可行方案和方法,利用非分散红外光度法技术,开发研制具有自主知识产权的适合国情的适于野外便携式的测油仪。利用此仪器,可以检测出被测水样中亚甲基、甲基物质和动植物油脂的污染物含量,为我国众多的环境检测站点监测水体的油污染状况提供依据。
### 内容概要 《计算机试卷1》是一份综合性的计算机基础和应用测试卷,涵盖了计算机硬件、软件、操作系统、网络、多媒体技术等多个领域的知识点。试卷包括单选题和操作应用两大类,单选题部分测试学生对计算机基础知识的掌握,操作应用部分则评估学生对计算机应用软件的实际操作能力。 ### 适用人群 本试卷适用于: - 计算机专业或信息技术相关专业的学生,用于课程学习或考试复习。 - 准备计算机等级考试或职业资格认证的人士,作为实战演练材料。 - 对计算机操作有兴趣的自学者,用于提升个人计算机应用技能。 - 计算机基础教育工作者,作为教学资源或出题参考。 ### 使用场景及目标 1. **学习评估**:作为学校或教育机构对学生计算机基础知识和应用技能的评估工具。 2. **自学测试**:供个人自学者检验自己对计算机知识的掌握程度和操作熟练度。 3. **职业发展**:帮助职场人士通过实际操作练习,提升计算机应用能力,增强工作竞争力。 4. **教学资源**:教师可以用于课堂教学,作为教学内容的补充或学生的课后练习。 5. **竞赛准备**:适合准备计算机相关竞赛的学生,作为强化训练和技能检测的材料。 试卷的目标是通过系统性的题目设计,帮助学生全面复习和巩固计算机基础知识,同时通过实际操作题目,提高学生解决实际问题的能力。通过本试卷的学习与练习,学生将能够更加深入地理解计算机的工作原理,掌握常用软件的使用方法,为未来的学术或职业生涯打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值