RednaxelaFX对《HotSpot实战》的笔记(32)
RednaxelaFX (Script Ahead, Code Behind)
在读 HotSpot实战
-
第250页 7.3 即时编译器 7.3.1 概述
很多人会误解-Xcomp的实际作用,而书中也只有这么一句而已。 我之前一篇笔记里有举例子说明HotSpot就算在-Xcomp模式下仍然可能(而且非常可能)用解释器来执行方法的:http://rednaxelafx.iteye.com/blog/1038324
书中第250页这段文字不幸几乎每句都有错⋯orz 后面第251页也有一句:
结合这句,书里对第0层的说明基本正确。在多层编译模式下,HotSpot的解释器确实只是选择性对方法做性能分析。书中提到了其中一个条件,而还有一些别的条件会触发解释器做性能分析,例如第3层C1编译失败但第4层C2仍然有机会编译该方法的话。 第1层是用C1编译,但是不做任何profiling所以不采集任何性能数据。一个Java方法一旦进入这一层执行基本上就一直停留在此了;只能通过逆优化回到第0层的解释器里,而无法向更高层迁移。用这层编译得到的代码跟在Client VM里的C1编译的基本一致;不过在JDK8的C1里新实现的消除数据边界检查和循环不变量外提优化只在Client VM的C1才做,而在Server VM的多层编译模式下不做,也就是说这个第1层比Client VM里的C1会稍微少做一点点优化。 第2层不是用C2而是用C1编译。它不使用任何性能数据来引导优化。它只是比第1层多了方法入口和循环计数器的代码而已,并且可以升级到更高层。 第3层是用C1编译,但它也不使用任何性能数据来引导优化。其实HotSpot VM的C1是故意比较保守不使用性能数据来优化代码的,以便代码更稳定。这一层编译在第2层的基础上增加了采集更细致的性能数据的代码,包括空指针检查、条件分支、虚方法调用、类型检查等。最近还新加了参数类型和返回值类型profiling。同时这一层编译会禁用部分优化,例如消除条件表达式和合并基本块这俩都不做。采集性能数据的额外开销和禁用部分优化带来的性能下降使得第3层会比第2层显著的慢,不过这还是比解释器采集性能数据要快多了。 第4层是用C2编译做完全的优化没错。 另外就是多层编译模式在JDK8的HotSpot Server VM才默认开启,在JDK7是没有默认开启的喔。原本是计划要在JDK7就默认开启,但Oracle内部做性能测试的时候发现在一些重要的测试中开启多层编译反而变慢了,所以推迟了。而JDK8用了个治标不治本的方式解决了那几个测试中性能下降的问题,所以默认就打开了。大家能猜出性能下降的原因不?>_< 我之前也有写过两篇笔记提到了HotSpot的多层编译系统: http://hllvm.group.iteye.com/group/topic/39806#post-260654 http://rednaxelafx.iteye.com/blog/1022095 引用我的笔记里的: 在多层编译模式下,HotSpot Server VM(此时应该叫做HotSpot Tiered VM了)会同时用上解释器、C1和C2: Tier 0:CompLevel_none,解释器 Tier 1:CompLevel_simple ,C1的正常编译(没有任何profiling)。这跟在Client VM里C1的工作模式几乎一样。进入该层的方法无法继续升级到更高层,除非先逆优化到解释器里。 Tier 2:CompLevel_limited_profile ,C1带基本profiling的编译(有方法调用和循环计数器,跟解释器用的计数器的位置和作用一样)。可以升级到更高层。 Tier 3:CompLevel_full_profile ,C1带所有profiling(包括空指针检查、条件分支、虚方法调用、类型检查等。最近还新加了参数类型和返回值类型profiling),同时禁用少量优化。可以升级到更高层。 Tier 4:CompLevel_full_optimization,C2编译。
-
第68页 2.3.6 init_globals函数:初始化全局模块
这里和后面第237页的
不知为啥书里都把Structured Exception Handling(SEH)给缩写成SHE了⋯手误吧。 话说回来,UseVectoredExceptions参数从JDK8开始deprecate,已经没有任何效果;到JDK9就会不再接受该参数。这个变更是 https://bugs.openjdk.java.net/browse/JDK-7188234 原本这个参数有效的时候也并不是在“Windows 64位平台上”可用,而是在“Windows Itanium(IA-64)”上才使用。即便在64位x86上HotSpot也不用它的。因为Oracle不再支持在Itanium上运行HotSpot VM,所以这个参数也就没用了。 关于“向量化异常处理”: http://msdn.microsoft.com/en-us/library/windows/desktop/ms681420.aspx
-
第82页 3.1.5 实战:用HSDB调试HotSpot
这小节内容看起来好眼熟⋯我的博文:http://rednaxelafx.iteye.com/blog/1847971 不过HSDB就是那么个简单的工具,要介绍它的话大概就是会截这么些图吧。眼熟倒也不奇怪。 可以看出书中的文字和图片都是作者自己整理的(至少不是从我那篇直接复制过去的),这点我很高兴 >_<
-
第14页 1.2.2 HotSpot源代码结构
嗯,跟我之前发过的笔记类似:http://hllvm.group.iteye.com/group/topic/26998#post-193368 不过不知道为啥书里这段的目录名大小写都不大对,所有目录名都写成大写开头了,而实际HotSpot代码里目录名都是全小写的。当然Windows对路径的大小写不敏感⋯ 第15页的:
adlc是Architecture Description Language Compiler,是平台描述文件的编译器。平台描述文件是各个平台相关目录(hotspot/src/cpu/<arch>/vm 或 hotspot/src/os_cpu/<os_arch>/vm)里的.ad后缀的文件。adlc负责把.ad文件“编译”为C++源码。生成出来的C++源码是C2的指令选择和寄存器分配的平台相关部分。
ci不是动态编译器啦,是compiler interface,具体来说是动态编译器访问VM运行时系统的一个抽象层。
-
第167页 5.1.4 栈上分配和逸出分析
嗯但是Oracle/Sun的HotSpot VM从来没在产品里实现过栈上分配,而只实现过它的一种特殊形式——标量替换(scalar replacement)。这俩是不一样的喔。栈上分配还是要分配完整的对象结构,只不过是在栈帧里而不在GC堆里分配;标量替换则不分配完整的对象,直接把对象的字段打散看作方法的局部变量,也就是说标量替换后就没有对象头了,也不需要把该对象的字段打包为一个整体。 其效果可以参考我之前的两篇笔记: http://rednaxelafx.iteye.com/blog/659108 http://rednaxelafx.iteye.com/blog/774673 另外我们在研发JVM的时候也发现其实在栈上分配Java对象带来的minor GC频率的降低并不是最大的受益点;TLAB的快速分配已经非常快了,而当young gen大小配置合理时minor GC的开销并不大。如果能做到标量替换就能更好的使用寄存器,以及允许JIT编译器做更有效的优化,这才是最重要的地方⋯ 话说Sun JDK6确实是从6u23开始默认开启逃逸分析的。 https://bugs.openjdk.java.net/browse/JDK-6873799 虽然代码变更似乎更早就放进去了,但在6u23之前的实际发布的产品里还是给禁用掉了么…
-
第255页 7.3.3 编译器的基本结构
前最笔误了…前缀。 不知道作者对ci的理解是怎样的呢?
-
第273页 8.2.2 数据传送指令
这段想表达的意思很明白,但细节写得有点怪。为啥最后一句会有“(或edi)”…?可能想说的是“(或eax)”吧。
-
第222页 6.2.6 优化:栈顶缓存
其实vtos的v是void,该TosState表示不缓存任何数据在寄存器里,而不是“缓存tos类型数据” >_< 操作数栈是空的时候,TosState肯定是vtos——没任何数据所以也没得好缓存的。而操作数栈不是空的时候也还是有可能进入vtos状态,例如说刚调用完一个返回类型为void的方法时。 关于栈顶缓存可以参考我以前写的一篇笔记:http://hllvm.group.iteye.com/group/topic/34814#post-231982 我以前写的JVM的演示稿也有提到,在第174-179页: http://www.valleytalk.org/2011/07/28/java-%E7%A8%8B%E5%BA%8F%E7%9A%84%E7%BC%96%E8%AF%91%EF%BC%8C%E5%8A%A0%E8%BD%BD-%E5%92%8C-%E6%89%A7%E8%A1%8C/
这段描述很容易让读者误解。transition()函数主要有两个作用: 1、文档:以代码为文档,标示出该模板期望的输入和输出栈顶状态; 2、正确性校验:在执行该函数来从模板生成具体代码的时候检查模板所声明的输入/输出类型与该函数所实现的是否一致。这就是书中所讲的。 但实际上这个transition()函数并不是说这个模板生成的代码(native code)就只能处理其指定的输入/输出状态。正好相反,它并不影响生成的native code。真正影响native code的是Template的_tos_in和_tos_out字段。 TemplateTable类有个静态成员变量,是所有字节码的Template实例的数组:
// The TemplateTable defines all Templates and provides accessor functions // to get the template for a given bytecode. class TemplateTable: AllStatic { // ... private: static Template _template_table [Bytecodes::number_of_codes]; // ... };
然后在TemplateTable::initialize()函数里会对这个数组里的每个Template实例初始化其状态,例如:
// Java spec bytecodes ubcp|disp|clvm|iswd in out generator argument def(Bytecodes::_iadd , ____|____|____|____, itos, itos, iop2 , add );
这个对TemplateTable::def()的调用就将iadd字节码对应的Template实例的_tos_in与_tos_out分别初始化为itos了。 TemplateTable::transition()函数在这个例子做的其实是检查TemplateTable::iop2()所实现的模板与iadd对应的模板实例里_tos_in与_tos_out两字段是否都匹配。但仅此而已。 真正在生成native code时,TemplateInterpreterGenerator::set_entry_points()调用到TemplateInterpreterGenerator::set_short_entry_points()、set_wide_entry_point(),里面就会设置好能匹配的TosState的入口。 回到iadd字节码的例子,它预期的进入状态是itos,跟它匹配的TosState可以是vtos和itos,
void TemplateInterpreterGenerator::set_short_entry_points(Template* t, address& bep, address& cep, address& sep, address& aep, address& iep, address& lep, address& fep, address& dep, address& vep) { switch (t->tos_in()) { // ... case itos: vep = __ pc(); __ pop(itos); iep = __ pc(); generate_and_dispatch(t); break; // ... } }
这里可以看到,实际生成的代码逻辑是,如果上个字节码执行后的TosState是vtos,那么到这个iadd的时候会从vep进入;如果上个字节码执行后是itos,那么从iep进入;其它TosState跟iadd不匹配,JVM要报错,它们的入口就会跳到报错的处理函数。这里vep跟iep之间有一个pop指令,将vtos调整到itos,这样就跟iep的地方所期待itos匹配上了。 换个方式说,对iadd的处理函数来说, (下面是Intel语法伪代码)
aep: // atos entry point lep: // ltos entry point fep: // ftos entry point dep: // dtos entry point goto illegal_bytecode; vep: // vtos entry point pop eax; // 进入状态是vtos,先把左操作数取到eax iep: // itos entry point,左操作数已经在eax pop edi; // 取右操作数到edi add eax, edi // 对左右操作数求和存到eax movz ebx, byte ptr: [esi+1] // 取下一条字节码 inc esi // 字节码指针自增1 jmp dispatchTable[itos][ebx] // 跳转到下一条字节码的处理函数
这样讲解一下,书里后面清单6-12所示汇编代码应该更好理解一点吧。 (书中引用的汇编是hsdis插件输出的,用的是AT&T语法)
实际上如果上一条字节码执行后TosState是itos的话,就会直接从书中例子的0x027df081: pop %edi进入,因为左操作数已经在栈顶缓存eax里了;而最后3条指令也不是“返回到上层调用环境”,而是直接跳转到下一条字节码的处理函数去,这是token-threaded解释器的特征。另外idd是iadd的笔误。 像这样个模板在进入的地方有适配代码的例子,我前面提到的JVM演示稿的第178-179页正好有个例子,有兴趣的读者可以参考一下。
嗯在HotSpot的整个解释器范围内确实是贯彻使用了栈顶缓存,效果相当不错。我以前写过个例子对比HotSpot与Dalvik的解释器,可以看到:在运用了栈顶缓存后,字节码是基于栈还是基于寄存器的对于真正运算来说执行效率并不会差多少,但是由于前者要表达同样逻辑比后者需要更多的字节码指令,在指令分派这块开销还是会比后者大一些: https://gist.github.com/rednaxelafx/759495 书后面像是第274页也有提到栈顶缓存。
-
第277页 8.2.3 实战:数组的越界检查
嗯是啊⋯联想到最近的heartbleed事件,又是缓存溢出惹的祸。
-
第298页 8.7.2 invoke系列指令
虽然我猜大部分熟悉Java字节码的人都知道作者这里想说的是什么,但这个表述方式实在不太好⋯ 并不是“在编译任何一条Java代码之前”,那个aload_0就是add()调用所包含的压参动作嘛。 假如有这样的例子:
public static int foo(char c, String str) { return str.indexOf(c); }
编译它得到的字节码会是类似:
aload_1 iload_0 invokevirtual java.lang.String.indexOf(C)I ireturn
这里传递“this”给indexOf()方法的字节码就不是aload_0而是aload_1了。其实没啥,谁是“接收”调用的对象,就把谁先压到操作数栈上,然后再压入显式参数。 之前在一篇笔记里码过相关的文字,不想重复码了:http://rednaxelafx.iteye.com/blog/652719
RednaxelaFX的其他笔记 · · · · · · ( 全部140条 )
- Advanced Virtual Machine Design and Implementation
- 1
- The C Programming Language
- 1
- Advanced Compiler Design and Implementation
- 2
- 计算机软件测试
- 1
- 编译原理 技术与工具
- 2
- 编译器构造
- 2
- Optimizing Compilers for Modern Architectures
- 4
- Modern Compiler Implementation in ML
- 8
- ふつうのコンパイラをつくろう
- 4
- Trustworthy Compilers
- 7
- The Compiler Design Handbook
- 5
- Oracle JRockit
- 3
- Java Performance
- 27
- Java Performance
- 1
- 冴えない彼女の育てかた 5
- 1
- Pro .NET Performance
- 3
- Engineering a Compiler, Second Edition
- 3
- A Retargetable C Compiler
- 1
- 2週間でできる! スクリプト言語の作り方
- 2
- 深入理解Java虚拟机(第2版)
- 6
- 深入嵌入式Java虚拟机
- 4
- 编译原理 技术与工具
- 1
- コーディングを支える技術 ~成り立ちから学ぶプログラミング作法
- 1
- 冴えない彼女の育てかた 4
- 2
- きつねさんでもわかるLLVM ~コンパイラを自作するためのガイドブック~
- 2
- 深入理解Android
- 13
- NO ANCIENT WISDOM, NO FOLLOWERS
- 1