`
IcyFenix
  • 浏览: 362520 次
  • 性别: 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了。

你想说第一种更快吧…

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

相关推荐

    while循环语句 sql server

    while循环语句 sql server

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

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

    SQL Server Native Client 11-sqlncli-11驱动

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

    一个简单的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设计的,旨在提高性能、安全性和兼容性,特别是...

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

    在这个“易语言源码循环语句的用法之速度比较”压缩包中,主要探讨的是易语言中不同循环语句在执行效率上的差异,这对于优化代码和理解编程性能至关重要。 在程序设计中,循环语句是控制流程的重要组成部分,它们...

    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"包含了...

    SQLServer2008Client

    这个工具集提供了多种功能,使得用户能够有效地管理和操作SQL Server数据库,包括数据查询、数据导入导出、数据库维护以及性能监控等。在了解SQL Server 2008 Client之前,我们需要先知道SQL Server 2008的基本概念...

    SQLServer2008Client绿色纯净版

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

    SQL Server 2000 client

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

    win10可用的sql server 2008 navicat client

    标题中的“win10可用的sql server 2008 navicat client”指的是在Windows 10操作系统中,能够与SQL Server 2008兼容的Navicat客户端工具。Navicat是一款强大的数据库管理和开发工具,它支持多种数据库系统,包括...

    SQLserver常用语句大全

    * WHILE 语句:循环语句 * WAITFOR 语句:等待指定的时间或事件 七、SELECT 语句 * SELECT 语句的基本语法:SELECT 列名 FROM 表名 WHERE 条件 * 使用 LIKE 操作符:SELECT * FROM 表名 WHERE 列名 LIKE '%find ...

    MSSQL性能监控SQL语句

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

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

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

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

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

    SQL SERVER 语句大全.rar

    SQL Server提供多种备份类型(完整、差异、日志),以及对应的恢复模式,确保数据安全性和业务连续性。 七、性能监控与调优 使用SQL Server Management Studio(SSMS)工具进行性能监控,通过查询分析器查看查询...

    SQL.rar_SQL语句_sql server

    5. **备份与恢复**:SQL Server有完善的备份和恢复策略,可以进行完整备份、增量备份和差异备份。Access的备份和恢复功能较为基础。 6. **开发工具**:SQL Server提供集成的开发环境SSMS(SQL Server Management ...

    SQLServer导出为Insert语句

    在IT行业中,数据库...总之,将SQL Server中的用户表数据导出为Insert语句是一项涉及数据库管理、编程和数据迁移的关键任务。通过MFC编写的应用程序,可以自动化这个过程,提高效率,同时确保数据的准确性和一致性。

    SQL服务器,性能分析语句

    常用的数据库性能分析语句,服务器卡的时候,常用的可以查看后台连接、使用能耗比较高的语句代码

Global site tag (gtag.js) - Google Analytics