`
IcyFenix
  • 浏览: 362871 次
  • 性别: Icon_minigender_1
  • 来自: 珠海
文章分类
社区版块
存档分类
最新评论

RE:循环语句的写法、Client和Server的性能差异以及microbenchmark的不准确

阅读更多
这篇文章也是个回帖,继续用来偷懒发blog
=====================================================

  一位朋友给了下面这段代码(在他给的代码中略作修改,避免了引入Random类、Integer装箱类导致不必要的因素),提出了2个问题:1.for (int i = 0, n = list.size(); i < n; i++)的写法是否会比for (int i = 0; i < list.size(); i++)更快?2.为何这段代码在Server VM下测出来的速度比Client VM还慢?
public class Client1 {
	public static void main(String[] args) {
		List<Object> list = new ArrayList<Object>();
		Object obj = new Object();
		// 填充数据
		for (int i = 0; i < 200000; i++) {
			list.add(obj);
		}
		long start;

		start = System.nanoTime();
		// 初始化时已经计算好条件
		for (int i = 0, n = list.size(); i < n; i++) {
		}
		System.out.println("判断条件中计算:" + (System.nanoTime() - start) + " ns");

		start = System.nanoTime();
		// 在判断条件中计算
		for (int i = 0; i < list.size(); i++) {
		}
		System.out.println("判断条件中计算:" + (System.nanoTime() - start) + " ns");
	}
}

  首先来看看在代码最终执行时,for (int i = 0, n = list.size(); i < n; i++)的写法是否会比for (int i = 0; i < list.size(); i++)更快,用于测试的虚拟机是:
D:\_DevSpace\jdk1.7.0\fastdebug\bin>java -version
java version "1.7.0-ea-fastdebug"
Java(TM) SE Runtime Environment (build 1.7.0-ea-fastdebug-b127)
Java HotSpot(TM) Client VM (build 20.0-b06-fastdebug, mixed mode)

  调试参数就加了一个-XX:+PrintAssembly,用于输出JIT后的汇编代码,虚拟机默认是Client。
  for (int i = 0, n = list.size(); i < n; i++)的循环体是
  0x01fcd554: inc    %edx               ; OopMap{[60]=Oop off=245}
                                        ;*if_icmplt
                                        ; - Client1::main@63 (line 17)
  0x01fcd555: test   %eax,0x1b0100      ;   {poll}
  0x01fcd55b: cmp    %eax,%edx
  ;;  124 branch [LT] [B5] 
  0x01fcd55d: jl     0x01fcd554         ;*if_icmplt
                                        ; - Client1::main@63 (line 17)

  变量i放在edx中,变量n放在eax中,inc指令对应i++(其实被优化成++i了),test指令是在回边处进行safepoint轮询(safepoint是用于进入GC,停顿线程),cmp是比较n和i的值,jl就是当i<n的时候进行跳转,跳转的地址是回到inc指令。
  for (int i = 0; i < list.size(); i++)的循环体是:
  0x01b6d610: inc    %esi
  ;;  block B7 [110, 118]

  0x01b6d611: mov    %esi,0x50(%esp)
  0x01b6d615: mov    0x3c(%esp),%esi
  0x01b6d619: mov    %esi,%ecx          ;*invokeinterface size
                                        ; - Client1::main@113 (line 23)
  0x01b6d61b: mov    %esi,0x3c(%esp)
  0x01b6d61f: nop    
  0x01b6d620: nop    
  0x01b6d621: nop    
  0x01b6d622: mov    $0xffffffff,%eax   ;   {oop(NULL)}
  0x01b6d627: call   0x01b2b210         ; OopMap{[60]=Oop off=460}
                                        ;*invokeinterface size
                                        ; - Client1::main@113 (line 23)
                                        ;   {virtual_call}
  0x01b6d62c: nop                       ; OopMap{[60]=Oop off=461}
                                        ;*if_icmplt
                                        ; - Client1::main@118 (line 23)
  0x01b6d62d: test   %eax,0x160100      ;   {poll}
  0x01b6d633: mov    0x50(%esp),%esi
  0x01b6d637: cmp    %eax,%esi
  ;;  224 branch [LT] [B8] 
  0x01b6d639: jl     0x01b6d610         ;*if_icmplt
                                        ; - Client1::main@118 (line 23)

  可以看到,除了上面原有的几条指令外,确实还多了一次invokeinterface方法调用(这里发生了方法调用,但是基本上没有分派的开销,因为inline cache是能起作用的),执行的方法是size,方法接收者是list对象,除此之外,其他指令都和上面的循环体一致。所以至少在HotSpot Client VM中,第一种循环的写法是能提高性能的。

  但是这个结论并不是所有情况都能成立,譬如这里把list对象从ArrayList换成一个普通数组,把list.size()换成list.length。那将可以观察到两种写法输出的循环体是完全一样的(都和前面第一段汇编的循环一样),因为虚拟机不能保证ArrayList的size()方法调用一次和调用N次是否会产生不同的影响,但是对数组的length属性则可以保证这一点。也就是for (int i = 0, n = list.length; i < n; i++)和for (int i = 0; i < list.length; i++)的性能是没有什么差别的。

  再来看看ServerVM和ClientVM中这2段代码JIT后的差别,本想直接对比汇编代码,但ServerVM经过Reordering后,代码就完全混乱了,很难和前面的比较,不过我们还是可以注意到两者编译过程的不同:

  这是Client VM的编译过程
VM option '+PrintCompilation'
    169   1       java.lang.String::hashCode (67 bytes)
    172   2       java.lang.String::charAt (33 bytes)
    174   3       java.lang.String::indexOf (87 bytes)
    179   4       java.lang.Object::<init> (1 bytes)
    185   5       java.util.ArrayList::add (29 bytes)
    185   6       java.util.ArrayList::ensureCapacityInternal (26 bytes)
    186   1%      Client1::main @ 21 (79 bytes)

  这是Server VM的编译过程
VM option '+PrintCompilation'
    203   1       java.lang.String::charAt (33 bytes)
    218   2       java.util.ArrayList::add (29 bytes)
    218   3       java.util.ArrayList::ensureCapacityInternal (26 bytes)
    221   1%      Client1::main @ 21 (79 bytes)
    230   1%     made not entrant  Client1::main @ -2 (79 bytes)
    231   2%      Client1::main @ 51 (79 bytes)
    233   2%     made not entrant  Client1::main @ -2 (79 bytes)
    233   3%      Client1::main @ 65 (79 bytes)

  ServerVM中OSR编译发生了3次,丢弃了其中2次(made not entrant的输出),换句话说,就是main()的每个循环JIT编译器都要折腾一下子。当然这并不是ServerVM看起来比ClientVM看起来慢的唯一原因。ServerVM的优化目的是为了长期执行生成尽可能高度优化的执行代码,为此它会进行各种努力:譬如丢弃以前的编译成果、在解释器或者低级编译器(如果开启多层编译的话)收集性能信息等等,这些手段在代码实际执行时是必要和有效的,但是在microbenchmark中就会显得很多余并且有副作用。因此写microbenchmark来测试Java代码的性能,经常会出现结果失真。
分享到:
评论
12 楼 RednaxelaFX 2011-06-30  
IcyFenix 写道
你是想说doTest()编译了2次吧。由于第一次OSR编译,顺便把方法编译的阈值溢出了,在第二次调用doTest()的时候触发了标准编译,这时候可以精确确定list的类型。

其实胜负关键是在OSR入口处,它的存在使得某些局部变量虽然明明只有一处定义,但却不得不经过一次phi节点,于是类型信息就乱掉了。

Client1的main方法,原本字节码的控制流图:


由HotSpot client compiler在OSR编译时转换为HIR之后的控制流图:

这B1前的入口就是真正带来差异的点。OSR入口使得B1的开头不得不带上了若干个phi节点,破坏了原本的类型信息的准确性。
11 楼 IcyFenix 2011-06-30  
RednaxelaFX 写道
我是针对Client2的代码来问的,其实是on-stack replacement,OSR。
可以留意一下Client2的例子运行的时候main()会被编译两次,第一次是OSR编译,第二次是标准编译

囧,你这个必须改为开放性试题,不能列固定答案,OSR太坑爹了,注意到发生了2次编译也不一定会去猜OSR那么常见的词汇啊。

你是想说doTest()编译了2次吧。由于第一次OSR编译,顺便把方法编译的阈值溢出了,在第二次调用doTest()的时候触发了标准编译,这时候可以精确确定list的类型。如果采用我修改的定义:
List<Object> list = flag ? new LinkedList<Object>() : new ArrayList<Object>();  
那在标准编译中,list的类型也无法精确推断出来。
10 楼 RednaxelaFX 2011-06-30  
IcyFenix 写道
那我猜那3个字母的组合是“Exact Type Inference”——精确类型推断。

Boo-boo...不对。
我是针对Client2的代码来问的,其实是on-stack replacement,OSR。
可以留意一下Client2的例子运行的时候main()会被编译两次,第一次是OSR编译,第二次是标准编译,两次生成的代码的差异。
不过最终造成list.size()没内联的直接原因确实是类型没足够细化,而造成无法细化的原因来自OSR所引入的phi节点。
9 楼 IcyFenix 2011-06-30  
好了,交作业了。
要解答的问题就是虚拟机是根据什么依据来完成list.size()的内联的。
既然撒迦提示了phi,而测试代码中,虽然List接口的实现类(LinkedList、ArrayList)都被虚拟机加载过,但是list对象的赋值是只有那么一次的,不需要做任何变换就满足SSA形式。那我们把它的定义改为:
List<Object> list = flag ? new LinkedList<Object>() : new ArrayList<Object>();

对应的doTest()改为doTest(boolean flag),JIT的输出就立即变成:
  0x0267df4c: inc    %esi
  ;;  block B10 [124, 132]

  0x0267df4d: mov    %esi,0x5c(%esp)
  0x0267df51: mov    0x40(%esp),%eax
  0x0267df55: mov    %eax,%ecx          ;*invokeinterface size
                                        ; - Client2::doTest@127 (line 24)
  0x0267df57: mov    %eax,0x40(%esp)
  0x0267df5b: nop    
  0x0267df5c: nop    
  0x0267df5d: nop    
  0x0267df5e: mov    $0xffffffff,%eax   ;   {oop(NULL)}
  0x0267df63: call   0x0263b210         ; OopMap{[64]=Oop off=1080}
                                        ;*invokeinterface size
                                        ; - Client2::doTest@127 (line 24)
                                        ;   {virtual_call}
  0x0267df68: nop                       ; OopMap{[64]=Oop off=1081}
                                        ;*if_icmplt
                                        ; - Client2::doTest@132 (line 24)
  0x0267df69: test   %eax,0x200100      ;   {poll}
  0x0267df6f: mov    0x5c(%esp),%esi
  0x0267df73: cmp    %eax,%esi
  ;;  256 branch [LT] [B11] 
  0x0267df75: jl     0x0267df4c         ;*if_icmplt

又变回了老样子,因为这时候虚拟机已经无法推断出list的实际类型了,alright,那我猜那3个字母的组合是“Exact Type Inference”——精确类型推断。

8 楼 RednaxelaFX 2011-06-30  
IcyFenix 写道
microbenmark

<< microbenchmark
7 楼 RednaxelaFX 2011-06-30  
IcyFenix 写道
呃,鸭梨很大啊
从你贴的ASM上看,list看起来已经挂了,但是无论是Escape Analysis还是Stack Allocations都不是三个字母滴缩写呀。先去吃饭,回来再做作业

list没挂啊。那段代码里%edi就是list局部变量分配到的寄存器。
0xc(%edi)

这用Intel语法写就是
[edi + c]

也就是
[list + #offset_of_size_field]

换回成Java语法那就是
list.size
6 楼 IcyFenix 2011-06-30  
呃,鸭梨很大啊
从你贴的ASM上看,list看起来已经挂了,但是无论是Escape Analysis还是Stack Allocations都不是三个字母滴缩写呀。先去吃饭,回来再做作业
5 楼 RednaxelaFX 2011-06-30  
  ;;  block B8 [118, 121]

  0x00bbe448: inc    %esi               ; OopMap{edi=Oop [48]=Oop off=1017}
                                        ;*goto
                                        ; - Client2::doTest@121 (line 22)
  0x00bbe449: test   %eax,0x970100      ;*goto
                                        ; - Client2::doTest@121 (line 22)
                                        ;   {poll}
  ;;  block B7 [107, 115]

  0x00bbe44f: mov    0xc(%edi),%ecx     ;*getfield size
                                        ; - java.util.ArrayList::size@1 (line 177)
                                        ; - Client2::doTest@110 (line 22)
  0x00bbe452: cmp    %ecx,%esi
  ;;  240 branch [LT] [B8] 
  0x00bbe454: jl     0x00bbe448         ;*if_icmpge
                                        ; - Client2::doTest@115 (line 22)

这是Client2的“第二个版本”的for循环的循环体被标准编译后生成的代码。分析交给你了 ^_^
嗯顺便一提,这种情况下仍然是“第一种”的代码快些。不过差别比Client1里看到的状况小多了。
4 楼 RednaxelaFX 2011-06-30  
嗯,那再来捧个哏。

把正文里的例子稍微改一下让它有正常编译的机会,同样是用HotSpot Client VM来跑可以看到不同的效果:
import java.util.ArrayList;
import java.util.List;

public class Client2 {
	public static void doTest() {
		List<Object> list = new ArrayList<Object>();
		Object obj = new Object();
		// 填充数据
		for (int i = 0; i < 1000000; i++) {
			list.add(obj);
		}
		long start;

		start = System.nanoTime();
		// 初始化时已经计算好条件
		for (int i = 0, n = list.size(); i < n; i++) {
		}
		System.out.println("判断条件中计算:" + (System.nanoTime() - start) + " ns");

		start = System.nanoTime();
		// 在判断条件中计算
		for (int i = 0; i < list.size(); i++) {
		}
		System.out.println("判断条件中计算:" + (System.nanoTime() - start) + " ns");
	}
  
	public static void main(String[] args) {
		doTest();
		doTest();
		doTest();
	}
}


Icy再来分析分析看?
提示:正文里client compiler没把list.size()内联掉,结合这个Client2版本来看又如何呢?猜猜看是什么东西导致了差异?
关键词有两个,一个是三个字母的缩写,另一个是“phi”
3 楼 IcyFenix 2011-06-30  
btw,吐槽的内容已修改到正文中~~~
2 楼 IcyFenix 2011-06-30  
嗯嗯,是想说第一种,写第二种笔误了。

行号对不上是因为import语句省略掉了,加上import和空行,所有语句都要向下顺移4句。
1 楼 RednaxelaFX 2011-06-30  
吐槽役参上

IcyFenix 写道
可以看到,除了上面原有的几条指令外,确实还多了一次invokeinterface方法调用(事实上这里也没有发生真正的调用,inline cache是能起作用的),执行的方法是size,方法接收者是list对象,除此之外,其他指令都和上面的循环体一致。所以至少在HotSpot Client VM中,第二种循环的写法是能提高性能的。

inline cache只是减少寻找目标方法(dispatch)的开销而已,“调用”还是发生了的。
真正削除了调用开销的那叫inline而不是inline cache
正好你在用fastdebug版,可以加个-XX:+TraceICs来看看。

HotSpot client compiler只利用了CHA来做虚方法调用的优化,而JDK初始化的时候就会加载ArrayList和LinkedList,从CHA来看List接口有多个实现,所以无法只通过CHA证明list.size()只有单一可能调用的目标,就只能用inline cache而不能inline了。

你想说第一种更快吧…

话说你的编译日志和帖出来的源码不对应嘛。行号对不上 = =

相关推荐

    Java 循环语句练习题.docx

    这个 Java 循环语句练习题文档旨在帮助初学者熟悉 Java 语言的循环语句,包括 for 循环、while 循环、do-while 循环等。通过练习题的解答,读者可以熟悉 Java 语言的基本语法和循环语句的使用方法。 一、for 循环...

    PB脚本中SQL语句写法与SQL中语句写法对照

    PB脚本中SQL语句写法与SQL中语句写法对照 PB脚本中SQL语句写法与SQL中语句写法对照是非常重要的...PB脚本中SQL语句写法与SQL中语句写法对照是非常重要的知识点,它们之间的差异和相似之处都需要我们认真研究和学习。

    SQL server Native Client 10.0

    SQL Server Native Client 10.0 是微软推出的一款专门用于与SQL Server 2008及后续版本交互的客户端库,它集成了ODBC(Open Database Connectivity)和OLE DB(Object Linking and Embedding, Database)接口。...

    SQL Server Native Client 10.0 驱动

    1. **性能优化**:SQL Server Native Client 10.0 针对SQL Server进行了优化,提供了快速的数据传输和查询执行。 2. **支持新特性**:包括对SQL Server 2008引入的新特性如FILESTREAM、透明数据加密(TDE)、Change ...

    SQL Server Native Client 11-sqlncli-11驱动

    SQL Server Native Client 11(简称sqlncli_11)是Microsoft开发的一款用于与SQL Server交互的客户端库,尤其适用于需要高性能数据访问的应用程序。它整合了ODBC(Open Database Connectivity)和OLE DB(Object ...

    Microsoft SQL Server 2005 Native Client

    综上所述,Microsoft SQL Server 2005 Native Client是一个全面的数据访问组件,为开发者提供了多种方式来连接和操作SQL Server,同时提供了高级功能和性能优化,确保了应用程序与数据库之间的高效通信。无论是在...

    一个简单的sql循环语句脚本

    SQL循环语句主要包括WHILE循环和FOR循环,它们允许我们在满足特定条件时重复执行一段代码块。在PL/SQL和T-SQL中,还有BEGIN-END结构来定义代码块。 1. WHILE循环:在满足特定条件时反复执行代码块,直到条件不再...

    Win10可用的Microsoft SQL Server 2008 Native Client

    Microsoft SQL Server 2008 Native Client是在Windows 10操作系统上使用的一种数据库连接组件,它为应用程序提供了与SQL Server交互的能力。这个组件是专为SQL Server设计的,旨在提高性能、安全性和兼容性,特别是...

    SQL Server Native Client 10.0.zip

    SQL Server Native Client 10.0 是微软针对SQL Server数据库管理系统推出的一款客户端接口,主要用于在Windows平台上连接和操作SQL Server 2008及其R2版本。这个压缩包"SQL Server Native Client 10.0.zip"包含了...

    SQL Server Native Client 9.0~11.0(32位和64位).rar

    SQL Server Native Client是微软开发的一款用于访问SQL Server数据库的客户端组件,它包含了ODBC(Open Database Connectivity)驱动程序和OLE DB提供程序。这个压缩包包含了9.0到11.0版本的32位和64位版本,适用于...

    SQLServer2008Client绿色纯净版

    SSMS提供了一个图形界面,用于执行SQL语句、创建和修改数据库对象、监控服务器性能以及配置服务器和数据库设置。用户可以通过SSMS进行以下操作: - **连接到服务器**:输入服务器名称和身份验证信息,建立与SQL ...

    SQL Server 2000 client

    它的客户端组件包括一系列实用程序和接口,使得用户能够远程连接到SQL Server服务器,执行SQL语句,管理数据库对象,如表、视图、存储过程等,以及进行数据备份和恢复操作。 1. **SQL Server Management Studio ...

    SQL语句实现跨Sql server数据库操作实例

    ### SQL语句实现跨SQL Server数据库操作实例 #### 背景介绍 在日常的数据库管理与开发工作中,经常会遇到需要在不同的SQL Server实例之间进行数据交换的情况。这些操作包括但不限于查询不同数据库中的数据、将数据...

    MSSQL性能监控SQL语句

    Microsoft SQL Server(MSSQL)作为一款广泛使用的数据库管理系统,提供了丰富的工具和方法来监控其性能。本文将深入探讨MSSQL性能监控中的几个关键SQL语句,帮助数据库管理员(DBA)和开发者更好地理解和管理MSSQL...

    Sql Server ExpressProfiler 监视Sql语句

    6. **性能调优**:通过分析SQL语句的执行计划,可以识别索引缺失、统计信息不准确等问题,从而对数据库进行针对性的优化。 7. **安全性审计**:Profiler还可以用来监控数据库的安全事件,例如权限变更、登录失败等...

    sqlserver自动生成sql语句工具sqlserver转oracle

    在IT行业中,数据库管理系统是核心组成部分,SQL Server和Oracle分别是微软和甲骨文公司推出的两款广泛应用的关系型数据库系统。在企业级应用中,有时需要在不同的数据库系统间进行数据迁移或兼容性处理,这就涉及到...

    易语言源码循环语句的用法之速度比较.rar

    本压缩包文件“易语言源码循环语句的用法之速度比较.rar”主要探讨的是易语言中不同循环语句的性能差异,这对于优化代码和提高程序运行效率至关重要。 在易语言中,循环语句是控制程序流程的重要组成部分,用于重复...

    循环语句的用法之速度比较

    本主题将深入探讨几种常见的循环语句,并对比它们在执行效率上的差异,这对于优化程序性能至关重要。易语言作为中国本土化的一种编程语言,同样支持多种循环结构,如“循环”,“直到”,“步进”等。以下我们将详细...

    SQL server 语句大全

    根据给定的文件标题、描述、标签以及部分内容,下面将详细介绍相关的SQL Server知识点: ### SQL语法 #### 创建数据文件 创建数据文件时,通常需要指定数据文件的逻辑名称、物理名称(包括完整的路径和文件扩展名...

    SQL Server 2005 技术内幕之性能调优原版CHM

    7. **备份与恢复策略**:阐述了备份和恢复对性能的影响,包括不同类型的备份(完整、差异、日志)和恢复模式(简单、完整、大容量日志),以及如何平衡备份速度和数据恢复能力。 8. **索引维护与重建**:讨论了索引...

Global site tag (gtag.js) - Google Analytics