文章目录
前言
最近公司企业微信给我们开通了极客时间的SVIP,我就顺便看了一些,突然就想起来之前一段时间找工作没有去记录,这次正好重新出发!
之前也写过一个类似的关于Java的JIT(即时编译器)知识整理虚拟机相关的知识,这次正好了解到他的另外一个特性, intrinsic
一、我是怎么了解到intrinsic
一天,我在认(mo)真(yu)工(hua)作(shui)的时候,突然看的一个注解,就正常的百度了下,一查不要紧,一堆不懂的,之后一发不可收拾,首先看的的是这个注解 @HotSpotIntrinsicCandidate,听说是JDK8有,就是比较少(JDK8有intrinsic - 直接看最后的参考资料),但是没有这个注解),找办法没找到,我就下了个JDK11,在String.java中找到了
这里顺便提一嘴,点进注解,才发现,是从JDK9开始有的
顺带提一嘴,因为以下原因
String的底层实现由JDK8的char[]变成了byte[]
二、由intrinsic我了解到什么
看完这些,其实开头所说的intrinsic就已经出现了,就是@HotSpotIntrinsicCandidate, 它的作用很简单,就是将使用这个注解的方法,在调用时,会被Hotspot虚拟机替换成高效的指令序列,忽略原本的实现。
而类似的,就像我在Java内存模型 -底层原理中提到的JVM的重排序
⚠️ 需要注意的是
- 并不是所有虚拟机都维护了同样的高效实现 —— 目前主流的商用虚拟机(HotSpot(Oracle)、J9 VM(IBM))
- 并不是所有的CPU架构都适用这些优化 —— 目前讨论的为x86架构(amd64同),像苹果的M系列以及其他的ARM架构的CPU就需要单独适配指令
- 如果没有对应的指令,就会直接使用JDK的实现
- 因为这些差异,这些优化才没有被直接放在源代码中
三、intrinsic与CPU指令
前面说那么多,优化,到底是怎么优化的
3.1 StringLatin1.indexof()
StringLatin1.indexof() 会被替换成 X86_64体系结构下的SSE4.2指令集中的PCMPESTRI —— 简单来说,就是它能够在16位以下的字符串中,查找另一个16字节以下的字符串,并返回命中的索引
3.2 Math中的大量方法
Java的Math类库中有着很多的intrinsic的方法,我门就看看其中的
Math.addExact —— 这个方法有两个重载分别位接收两个int和long值,并返回相加的值,当结果和溢出时,抛出 ArithmeticException(“integer overflow”);
/**
* Returns the sum of its arguments,
* throwing an exception if the result overflows an {@code int}.
*
* @param x the first value
* @param y the second value
* @return the result
* @throws ArithmeticException if the result overflows an int
* @since 1.8
*/
@HotSpotIntrinsicCandidate
public static int addExact(int x, int y) {
int r = x + y;
// HD 2-12 Overflow iff both arguments have the opposite sign of the result
if (((x ^ r) & (y ^ r)) < 0) {
throw new ArithmeticException("integer overflow");
}
return r;
}
在Java层面判断两个值的和是否超过最大值对应的就是两个异或、一个和与一个比较操作,而在X86_64体系中,大部分计算指令都会更新状态寄存器(Flags register), 其中就有表示是否溢出的标志位(overflow flag),因此我吗只需要在操作完成后,比较标志位即可。
3.3 Integer.bigCount()
这个方法是统计所输入int值的二进制形式有多少个1
/**
* Returns the number of one-bits in the two's complement binary
* representation of the specified {@code int} value. This function is
* sometimes referred to as the <i>population count</i>.
*
* @param i the value whose bits are to be counted
* @return the number of one-bits in the two's complement binary
* representation of the specified {@code int} value.
* @since 1.5
*/
@HotSpotIntrinsicCandidate
public static int bitCount(int i) {
// HD, Figure 5-2
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
return i & 0x3f;
}
可以看的,源码中的实现方式通过二进制的计算应该也是比较高明的,但是相对于X86_64体系架构中的一条指令popcnt就可以计算出1的个数,还是更加高效的。
3.4 Unsafe 类中经常会被用到的便是 compareAndSwap 方法
compareAndSwap()其实就是我们经常说的乐观锁思想的CAS的实现, 在JDK9+,被更名为 compareAndSet 或 compareAndExchange 方法。
在 X86_64 体系架构中,对这些方法的调用将被替换为lock cmpxchg 指令,也就是原子性更新指令。
3.5 String类、StringLatin1类、StringUTF16类和Arrays类的方法。
HotSpot 虚拟机将使用 SIMD 指令(single intruction multiple data,即用一条指令处理多个数据)小标题的类进行优化
For example,Arrays.equals(byte[], byte[])方法原本是逐个字节比较,在使用了 SIMD 指令之后,可以放入 16 字节的 XMM 寄存器中(甚至是 64 字节的 ZMM 寄存器中)批量比较。
四、intrinsic与方法内联
Hotspot虚拟机中,intrinsic实现方式分为两种
- 独立程序桩,就是在解释或者即时编译时调用,这类实现形式较少,主要为Math类中的一些方法
- 特殊的编译器IR —— 中间表达形式(Intermediate Representation)节点
在编译过程中,即时编译器使用特殊的IR节点替换原有IR节点,被后端使用,生产CPU指令,这是大部分intrinsic实现的方式。
这个替换过程是在方法内联时进行的,当即时编译器碰到方法调用,会先查询方法是否为intrinsic。如果是,插入特殊IR,不是,进行原本内联的工作。
内联不仅将被调用方法的IR图节点复制到调用者方法的IR图中,还要完成其他操作。被调用方法的参数替换为调用者方法进行方法调用时所传入参数。
五、现有的intrinsic
详情直接查看参考资料7
参考资料
- 22 | HotSpot虚拟机的intrinsic
- java ir_基本功 | Java即时编译器原理解析及实践
- 为什么int型最大值加一后等于这个值?
- [译]看JVM如何用最酷的x86指令来比较字符串
- java ir_基本功 | Java即时编译器原理解析及实践
- Compact Strings
- Java 12 OpenJDK / jdk / hs
- Java 8 OpenJDK / jdk8u / jdk8u / hotspot