在本文中,我们将学习什么是intrinsics(内部/内置函数),以及它们如何在Java和其他基于JVM的语言中工作。

什么是内置?

内置函数是由我们的编程语言的编译器或解释器进行特殊处理的函数。更具体地说,这是一种特殊情况,因为各种原因,编译器或解释器可以用替代实现替换函数。

编程语言通常通过理解一个特定的方法调用是特殊的来处理这个问题,无论何时我们调用这个方法,结果都是不同的。这样一来,我们的代码看起来与正常代码没有什么不同,但编程语言的实现可以在特殊情况下进行干预,以提供额外的好处。

它的具体工作方式因编程语言、操作系统和硬件而异。然而,由于这些都是为我们处理的,我们通常不需要知道这些细节中的任何一个。

本质可以带来各种好处。用本机代码替换特定算法可以使它们性能更好,甚至可以利用操作系统的特定功能或底层硬件。

JVM上的内置函数

JVM通过用替代版本替换精确类上的精确方法调用来实现内置函数。JVM自己处理这个问题,所以它只适用于核心类和特定的体系结构。它还只允许交换某些方法,而不是整个类。

具体的工作方式在JVM之间会有所不同。这不仅包括JVM的不同版本,例如Java 8和Java 11。这还包括不同的JVM目标——例如Linux和Windows——尤其是JVM供应商——Oracle和IBM。在某些情况下,传递给JVM的某些命令行标志可能会影响它们。

这种多样性意味着无法仅基于应用程序来确定哪些方法将被内置方法替换,哪些不会。这将根据运行应用程序的JVM而有所不同。但在某些情况下,这可能会导致令人惊讶的结果——包括通过简单地更改所使用的JVM而获得的显著性能优势。

性能收益

intrinsic通常用于实现同一代码的更高效版本,例如,通过利用正在运行的OS或CPU的实现细节。有时这是因为它可以使用更高效的实现,而有时它可以使用特定于硬件的功能。

例如,HotSpot JDK对java中的许多方法都有一个内在的实现java.lang.Math。根据具体的JVM,它们可能使用CPU指令来实现,以完成所需的精确计算。

一个简单的测试将证明这一点。例如,以java.lang.Math.sqrt()为例。我们可以编写一个测试:

for (int a = 0; a < 100000; ++a) {
    double result = Math.sqrt(a);
}

此测试将执行100000次平方根运算,大约需要123ms。但是,如果我们用Math实现的副本替换这段代码。Math.sqrt()

double result = StrictMath.sqrt(a);

这段代码执行同样的操作,但执行时间为166ms。通过复制实现,而不是允许JVM用内部版本替换它,这将增加35%

不可能的实现

在其他情况下,intrinsic用于代码无法在Java中实现的情况。这些通常是为非常低级的情况保留的。

例如,让我们看看java.lang.Thread线程类中的onSpinWait()方法。此方法表示此线程当前未执行任何工作,并且可以将CPU时间分配给另一个线程。为了实现这一点,它需要在尽可能低的水平上工作。

HotSpot JDK for x86体系结构使用PAUSE操作码直接在CPU上实现这一点。实现这一点的唯一其他方法是使用对本机代码的JNI调用,而这其中涉及的开销首先会抵消调用的好处。

识别Java中的Intrinsics语言

不幸的是,没有确定的方法来识别可能被内在版本取代的方法。这是因为不同的JVM,甚至是不同平台上的同一个JVM,会为不同的方法实现这一点。

但是,从Java 9开始使用HotSpotJVM时,所有可能被替换的方法都会使用@HotSpotInTrensicAndidate注释。添加此注释不会自动导致方法被替换。实际上,这种情况发生在底层JVM中。相反,JVM开发人员知道这些方法是特殊的,需要小心使用。

如果其他JVM被识别出来,它们可能会以不同的方式处理这个问题。这包括Java8或更早版本中的Hotspot JVM。

总结

我们无法编写依赖于内置函数存在的程序,因为无法知道它们是否在运行时JVM上可用。然而,它们是JVM可以用来改进程序工作方式的一种引人注目的方法。

这些内在特性可以——而且经常被——添加到新版本的JVM中。因此,只需升级我们正在运行的JVM,就可以改进我们已经运行的代码,所以这是确保我们保持依赖项和运行时最新的另一个原因。