`

关注性能: 宏性能基准测试(字节码提供了应用程序性能的线索)

阅读更多

    热衷于 Java 性能的 Jack Shirazi 和 Kirk Pepperdine ―― JavaPerformanceTuning.com 的董事和 CTO ―― 跟踪遍布 Internet 上的性能讨论,探究是什么在困扰着开发人员。在浏览 Usenet 新闻组 comp.lang.java 时,他们遇到了几个有意思的底层性能调整问题。在关注性能的这篇文章中,他们对字节码作了一些分析,检验并回答了其中的一些问题。

尽管没有专门针对 Java 性能的 Usenet 讨论组,但是有许多关于性能调整和优化的讨论。这些讨论中很大一部分基于从宏性能基准测试中得到的结果,所以在本月的专栏中我们也准备谈论有关宏基准测试(microbenchmarking)的好处和不足之处。

前置还是后置?

有一个问题特别引起我们注意:哪一种运算更快: i++ 还是 ++i ?在我们浏览过的几乎每一个论坛中都可以看到以不同的形式提出的这个问题。虽然这个问题很简单,但是看来没有一个绝对的答案。

首先介绍一下它们的区别, ++i 使用 前置增量运算符,而 i++ 使用 后置增量运算符。虽然它们都增加变量 i ,但是前置增量运算符在增量运算之前返回 i 的值,而后置增量运算符在增量运算之后返回值 i 。一个简单的测试程序展示了这种区别:

public class Test {
  public static void main(String[] args) {
    int pre = 1;
    int post = 1;
    System.out.println("++pre  = " + (++pre));
    System.out.println("post++ = " + (post++));
  }
}


运行 Test 类生成以下的输出:

++pre  = 2
post++ = 1






宏基准测试

难道不能试着反复运行每次运算,并观察哪一种运算有更快的运行时吗?简单的回答是能,但是危险在于宏基准测试并不总是测量您想要它们测量的内容。相当多的时候,即时(JIT)编译器的优化和变化掩盖了底层性能中所有可检测的差异。例如,一个这种测试显示第二个 i++ 运算比第一个 ++i 测试更快。但是改变测试顺序显示正好相反的结果!从这里我们只能得出测试方法有缺陷的结论。进一步的调查表明,这种令人困惑的结果来源于在第一次测试时发生的 HotSpot 优化。这些优化有双重效果:使第一次运行增加了额外的开销,并去掉了第二次运行时的解释成本。

宏基准测试的其他变化,如在发生了 JIT 启动成本后重复测试,在反复运行时只能给出不确定的结果。它可能告诉我们两种运算符在速度上没有区别,但是我们对此不能确定。

iinc 字节码运算符

Heinz Kabutx 博士在其新闻信 The Java Specialists Newsletter 中,问他的读者哪一个更快: i++ 、 ++i 还是 i+=1 ?在 Issue 64 中,他报告说有一位读者用一种简单的技术回答了他的问题:查看编译的字节码。事实上,他考察了四种增量语句:

++i;
i++;
i -= -1;
i += 1;


可以用 Java SDK 所带的反汇编程序 javap 很容易地分析编译的字节码。这四种增量语句的每一种得到的字节码都是 iinc 1 1 。

iinc 运算符有两个参数。第一个参数指定变量在 JVM 的局部变量表中的索引,第二个参数指定变量的增量值。

这是不是给了我们一个明确的回答?无论如何,如果不同的源代码编译为同样的字节码,那么在速度上没有区别,是不是?




运算符上下文

那么,如果代码片断都编译为同样的字节码,使用不同的运算符的意义何在?好,让我们回过头看前置增量符和后置增量符。关键的一点是什么时候访问变量。如果不访问变量,那么这些运算符之间就没有什么区别。语句 i++ 和 ++i 本身在功能上是一样的。不过,语句 j=i++ 和 j=++i 在功能上是 不一样的。我们需要分析在额外的赋值上下文中的字节码。考虑这两个类似的方法:

public static int preIncrement() {
  int i = 0, j;
  j = ++i;
  return j;
}
public static int postIncrement() {
  int i = 0, j;
  j = i++;
  return j;
}


反汇编 preIncrement() 和 postIncrement() 得到下面的字节码:

Method int preIncrement()
   0 iconst_0   
   1 istore_0
   2 iinc 0 1
   5 iload_0
   6 istore_1
   7 iload_1
   8 ireturn
Method int postIncrement()
   0 iconst_0
   1 istore_0
   2 iload_0
   3 iinc 0 1
   6 istore_1
   7 iload_1
   8 ireturn


现在我们 可以 看到这两种方法间的区别: preIncrement() 返回 1,而 postIncrement() 返回 0 。让我们分析字节码,更好地理解这种区别。首先,我们将解释在反汇编的代码中可以看到的不同字节码运算符。

字节码运算:i=0

iconst_0 运算符将整数 iconst_0 推到堆栈上。要完全理解这一点,请记住 JVM 模拟一个基于堆栈的 CPU(如果您以前没接触过堆栈,请参阅 java.util.Stack 类文档)。JVM 在需要以后对某些东西进行操作时,先将它们推到堆栈中,在准备对它们进行操作时弹出它们。

在 Java 语言中有几种不同的数据类型,对于不同的数据类型有不同的字节码运算符。对于某些特定的优化,值 -1、0、1、2、3、4 和 5 都有专门的字节码。如果我们不是处理这些值,那么编译器会生成 bipush 字节码运算,将一个特定的整数推到堆栈上(例如,如果方法的第一条语句是 int i = -2 ,那么第一个字节码将会 bipush -2 )。

下一条语句 istore_0 看上去可能像另一个处理整数 -1 到 5 的特殊字节码,但是事实上,这次 _0 指向一个到局部变量表的索引。JVM 维护一个局部于方法的变量表,字节码 istore 在堆栈的顶部弹出这个值,并将这个值储存到局部变量表中。在这里我们用的是 istore_0 ,所以这个值储存在表的索引 0 处。

所有这些解释针对的是“ i=0 ”的Java 字节码,它被转换为字节码:

0 iconst_0
1 istore_0


更多的字节码运算

现在我们知道了堆栈和局部变量表,我们可以更快地讨论其他字节码。正如我们前面说的,字节码 iinc 0 1 在局部变量表索引 0 处增量值 1, iload_0 将局部变量表索引 0 处的值推到椎栈中,而 ireturn 从堆栈中弹出这个值,并将它推到调用方法的操作数堆栈上。下面的表 1 概括了字节码。

表 1. 字节码

字节码     描述
iconst_0    将 0 推到堆栈中
iconst_1    将 1 推到堆栈中
istore_0    从堆栈中弹出这个值,并将它存储到局部变量表的索引 0 处
istore_1    从堆栈中弹出这个值,并将它存储到局部变量表的索引 1 处
iload_0    将局部变量表索引 0 处的值推到堆栈中
iload_1    将局部变量表索引 1 处的值推到堆栈中
iadd    从操作数堆栈中弹出两个整数并让它们相加。将得到的整数推回堆栈中
iinc 0 1    局部变量表索引 0 处的变量加 1
ireturn    从堆栈中弹出值并将它推到调用方法的操作数栈中。退出方法

比较方法

现在,让我们再看一下这些反汇编的字节码。我们将用 lvar 表示局部变量表,就像它是一个 Java 数组,并对字节码加上注释:

Method int preIncrement()
   0 iconst_0     //push 0 onto the stack
   1 istore_0     //pop 0 from the stack and store it at lvar[0], i.e. lvar[0]=0
   2 iinc 0 1     //lvar[0] = lvar[0]+1 which means that now lvar[0]=1
   5 iload_0      //push lvar[0] onto the stack, i.e. push 1
   6 istore_1     //pop the stack (value at top is 1) and store at it lvar[1], i.e. lvar[1]=1
   7 iload_1      //push lvar[1] onto the stack, i.e. push 1
   8 ireturn      //pop the stack (value at top is 1) to the invoking method i.e. return 1
Method int postIncrement()
   0 iconst_0     //push 0 onto the stack
   1 istore_0     //pop 0 from the stack and store it at lvar[0], i.e. lvar[0]=0
   2 iload_0      //push lvar[0] onto the stack, i.e. push 0
   3 iinc 0 1     //lvar[0] = lvar[0]+1 which means that now lvar[0]=1
   6 istore_1     //pop the stack (value at top is 0) and store at it lvar[1], i.e. lvar[1]=0
   7 iload_1      //push lvar[1] onto the stack, i.e. push 0
   8 ireturn      //pop the stack (value at top is 0) to the invoking method i.e. return 0


现在,希望您能更清楚地了解所发生的事情,以及方法之间的一些功能差别。惟一的差别是两个方法的第三个和第四个字节码交换了。注释的字节码清楚表明,在 postIncrement() 方法中, iinc 运算完全是多余的,因为从这一点起,不再使用被更新的局部变量元素 lvar[0] 。对于这个特定的方法,一个优化 JIT 编译程序可以完全去掉这种字节码运算。所以在这种特定情形中, postIncrement() 方法可能有比 preIncrement() 操作更少的字节码运算,从而使它更加高效。但是在大多数使用后置增量运算符的情况下,增量运算是不能优化的。




那么谁更快呢?

我们学到了什么?是的,如果语句只有 ++i 和 i++ ,那么它们之间没有区别。只有在存在额外的赋值时,编译的字节码才会有区别。

在赋值的上下文中,比较前置增量运算符或者后置增量运算符的使用有可能得到不同的运行时。但是使用哪种运算的功能结果都不太可能是一样的。记住,在我们这里的例子里,方法实际上返回不同的值,它取决于我们是使用前置增量运算符还是后置增量运算符。在一个普通程序中,其中一种变化可能会成为一个缺陷。




结束语

在过去,我们可以根据一组运算的语言表达对它们的成本进行测量。这是因为这些运算到底层运行时环境的转换总是静态的,这在 Java 运行时中是不成立的。Java 运行时可以动态优化运行的代码,这是一种特别强大的功能。尽管这种功能还没有使我们完全不能进行宏性能基准测试,但是它导致我们在使用这种技术时需要更加当心。

 

 

 

 

 

 

 

 

转自http://www.ibm.com/developerworks/cn/java/j-perf12053/

分享到:
评论

相关推荐

    基于虚拟机字节码注入的Android应用程序隐私保护机制.pdf

    "基于虚拟机字节码注入的Android应用程序隐私保护机制" 本文提出了一种基于虚拟机字节码注入技术的 Android 应用程序权限访问控制方法,以解决 Android 应用权限机制的滥用问题。该方法可以根据用户的安全需求和...

    基于Soot的JAVA字节码优化及性能分析.pdf

    本文主要讨论基于Soot的JAVA字节码优化及性能分析,旨在提高Java应用程序的性能。Java语言作为一种流行的编程语言,具有平台无关性、执行安全性、垃圾收集等特点,广泛应用于各种领域。但是,与C/C++等语言相比,...

    字节码实战包含class,字节码.zip

    "字节码实战"的主题深入探讨了字节码的概念、生成以及如何利用它来优化Java应用程序。下面将详细阐述相关知识点。 1. **字节码简介**: 字节码是Java源代码经过编译器编译后的中间表示形式,它是一种平台无关的二...

    Java Classloading Mechanism : ClassLoader & ASM & 动态字节码增强

    总结起来,Java的类加载机制保证了程序的稳定运行,而ASM库则提供了对字节码的直接操作能力,使得我们能够在运行时动态地修改类的行为。掌握这两者,开发者可以更好地理解和定制Java应用的运行过程,提升程序的灵活...

    Android中修改运行时内存Dalvik字节码

    在Android系统中,Dalvik虚拟机是Android应用执行的核心组件,它负责解析并执行APK中的 Dex(Dalvik Executable)文件,这是Android应用程序的字节码格式。本篇文章将深入探讨如何在Android运行时环境中修改Dalvik...

    可变字节码

    字节码通常是指计算机程序的一种中间表示形式,它是由编译器生成的,介于高级语言和机器语言之间,以单个字节为单位。然而,可变字节码并不局限于单个字节,它的核心在于根据数值的大小动态地选择字节数来编码,从而...

    Android字节码插桩

    在Android开发领域,字节码插桩是一种非常重要的技术,它允许开发者在程序运行时动态地插入代码,以此实现如性能监控、日志记录、权限检查等附加功能。ASM是Java字节码操控和分析框架,它在Android字节码插桩中扮演...

    修改字节码 jclasslib

    理解并操作字节码对于优化程序性能、调试、逆向工程以及插桩技术等具有重要意义。jclasslib是一款强大的开源字节码查看和分析工具,它为我们提供了一个直观的方式来探索和理解字节码的结构。 一、jclasslib概述 ...

    行业分类-设备装置-基于Java字节码的大型应用回归测试信息处理方法.zip

    在进行回归测试时,通过分析和操作字节码,我们可以对应用程序进行更深层次的测试和调试。 大型应用的回归测试通常面临两个主要挑战:一是测试覆盖率高,确保所有关键功能和逻辑都经过验证;二是测试效率,因为大型...

    cglibJava字节码生成库

    【标题】:“cglib——Java字节码生成库”是一个强大的工具,用于在运行时动态创建和修改类的字节码。它是一个广泛应用于Java开发中的库,特别是那些需要底层控制对象实例化、方法调用等场景的项目。 【描述】:...

    轻松看懂Java字节码.pdf

    开发者能够通过分析字节码来发现潜在的问题,改进程序性能,或者增强应用的安全性。 在实际分析Java字节码时,可以利用javap工具查看生成的字节码指令,例如以Main.class文件为例,会看到一系列的数字和字符组合。...

    JAVA字节码操作库 BCEL

    3. **代码优化**:通过修改字节码,BCEL可以实现代码的优化,例如去除无用代码、减少跳转指令等,提升程序执行效率。 4. **逆向工程**:BCEL可以用于Java类的反编译,虽然不如专用的反编译工具如JD-GUI详细,但可以...

    Lua字节码分析工具

    4. **性能分析**:通过对字节码的分析,可以识别出性能瓶颈,为优化代码提供依据。 5. **学习和教学**:对于初学者,这样的工具可以帮助理解Lua虚拟机的工作原理,加深对语言特性的理解。 ChunkSpy-0.9.8是这个...

    Java字节码加密工具.zip

    Java字节码加密工具是一种用于保护Java应用程序源代码安全的技术,它可以防止恶意用户逆向工程分析你的代码,从而窃取商业机密或者篡改程序逻辑。在这个名为"Java字节码加密工具.zip"的压缩包中,包含了一个名为...

    软件测试实验报告(使用LoadRunner进行性能测试实验)

    LoadRunner是一个功能强大的性能测试工具,提供了丰富的功能和特性,包括: 1. Virtual User Generator:录制脚本,模拟用户的行为。 2. Controller:配置场景,控制虚拟用户和虚拟用户所使用的机器。 3. Load...

    4.程显峰--Java字节码技术1

    Java字节码技术是Java平台的核心组成部分,它与Java虚拟机(JVM)紧密相连,为各种编程语言在Java平台上提供了可移植性和高效执行的基础。本篇将详细讲解Java字节码的概念、用途以及JVM如何执行字节码。 首先,让...

    jvm字节码自动加载

    了解JVM的字节码自动加载机制对于优化Java应用程序的性能、理解和诊断问题都极其重要。例如,通过使用VisualVM(压缩包中的visualvm_14可能是一个VisualVM的版本),我们可以监控和分析JVM的类加载行为,包括类加载...

    ASM 字节码修改工具中文帮助手册

    综上所述,ASM 4.0 是一个强大且灵活的 Java 字节码操作框架,提供了丰富的功能和工具,适用于多种应用场景,如程序分析、代码生成和转换等。无论是对于开发人员还是研究人员来说,都是一款不可或缺的工具。

    HelloWorld的javap -verbose HelloWorld 字节码初探

    标题中的“HelloWorld的javap -verbose HelloWorld”指的是在Java编程环境中,通过`javap`这个命令行工具来反汇编一个简单的“HelloWorld”程序,以深入理解字节码的工作原理。`javap`是Java Platform Debugger ...

Global site tag (gtag.js) - Google Analytics