提高大数据应用程序中 Java 的性能





0/5 (0投票)
提高大数据应用程序中 Java 的性能
立即免费获取 Intel® Parallel Studio XE!
Java* 自二十多年前问世以来,已成为一种流行的企业语言。最初因其“一次编写,随处运行”的特性而受到青睐,后来由于集成现有 Java 项目在构建新应用程序方面的相对便捷性,其使用量不断增加。Apache Hadoop* 等多个开源 Java 应用程序栈的增长进一步促进了这种增长。从根本上说,开发人员认为 Java 能够提高他们的生产力。
Java 程序员依赖 Java 虚拟机* (JVM*) 来实现“一次编写,随处运行”的承诺。JVM 也提供高性能。相比之下,C 和 C++ 开发人员依赖编译器。对于这些语言,编译不是运行时性能的关键路径,并且相对很少发生,因此编译器可以进行深度分析并提供专门的优化。另一方面,JVM 使用即时 (JIT) 编译模型。编译是动态的,在每次运行时都会发生,因此它需要轻量级但尽可能复杂。多年来,在优化 JIT 编译器方面付出了大量努力,对于一般的业务处理,Java 的性能已接近 C 或 C++ 提供的性能。
当今的企业应用程序,例如分析和深度学习,需要大量的数值计算。这些用例在执行大量矩阵数学运算的同时,都会消耗海量数据。Java 应用程序非常适合用于编排和交付数据。毫不夸张地说,大数据在 JVM 上运行。然而,大量的浮点数学运算更自然地属于用 C 或汇编编写的手动调整内核的范畴。改进 Java 生态系统以处理数值计算将使开发人员更具生产力。
大数据应用程序、分布式深度学习程序和人工智能解决方案可以直接运行在现有的 Apache Spark* 或 Hadoop 集群之上,并能受益于高效的横向扩展。运行在 Spark 框架之上的机器学习算法,以及当前 JVM 上数据科学的革命,推动了对 Java 中增强的单指令多数据 (SIMD) 支持的需求。SIMD 支持将为高性能计算 (HPC)、基于线性代数的机器学习 (ML) 算法、深度学习 (DL) 和人工智能 (AI) 等领域探索新机遇开辟道路。
为了说明这一点,让我们考虑 Open JDK9* 中新提供的 FMA (Fused Multiply-Add) API,并探索其在 Intel® 高级向量指令集 (Intel® AVX) 支持的 Intel® Xeon Phi™ 处理器 上运行的几个 ML 算法中的 Java 性能。
融合乘加 (FMA) 操作
现代 Intel® 处理器包含 Intel AVX 指令,用于执行 SIMD 操作,包括 FMA 操作 (A=A*B+C)。FMA 对于基于线性代数的 ML 算法、深度学习和神经网络(点积、矩阵乘法)、金融和统计计算模型以及多项式评估非常有价值。FMA 指令对 IEEE-754-2008 浮点值执行向量化的 a*b+c 操作,其中 a*b 乘法以无限精度执行。加法的最终结果被舍入以产生所需的精度。FMA 指令自第二代及更高版本的 Intel® 酷睿™ 处理器 起即可使用。要利用它们进行 SIMD 操作,JVM JIT 编译器需要将 Java 中编写的 FMA 操作映射到底层 CPU 平台上可用的 Intel AVX FMA 扩展。
Java 9 中提供的 FMA API
Open JDK9 将 FMA API 作为 `java.lang.math` 包的一部分提供给 Java 开发人员。Open JDK9 版本包含针对 Intel AVX FMA 扩展的编译器内在函数,这些函数可将 FMA Java 例程直接映射到现代 CPU(例如,Intel Xeon Phi 和 Intel® Xeon® Platinum 8180 处理器)上的 CPU 指令。这无需开发人员进行任何额外工作。然而,设计用于使用 FMA 指令的 Java 算法应考虑到,非 FMA 顺序的打包浮点乘法和加法指令可能与 FMA 产生略有不同的结果。在制定收敛标准时,重要的是要考虑中间结果精度的差异,以避免最终结果出现意外。在 Java 中,当 `a*b+c` 被评估为常规浮点表达式时,会涉及两次舍入错误:第一次是乘法运算,第二次是加法运算。
在 Java 中,通过 Math FMA API 支持 FMA 操作。FMA 例程返回三个参数的 FMA。它返回前两个参数的精确乘积,与第三个参数相加,然后一次舍入到最近的双精度值。FMA 操作使用 `java.math.BigDecimal` 类执行。`BigDecimal` 不支持无穷大和 NaN 算术输入值;这些输入通过两次舍入来处理,以计算正确的结果。
FMA API 接受浮点输入 a、b 和 c,并返回浮点类型。它同时支持单精度和双精度。FMA 计算以双精度执行(稍后讨论)。该实现首先筛选并处理 `BigDecimal` 不支持其算术的非有限输入值。如果所有输入都在有限范围内,则以下表达式通过显式将其转换为 `BigDecimal` 对象来计算浮点输入 a 和 b 的乘积
BigDecimal product = (new BigDecimal (a)).multiply (new BigDecimal (b));
在特殊情况下,当第三个浮点输入 c 为零时,API 会小心处理零的符号,以防乘积 (a*b) 也为零。最终结果零的符号通过以下浮点表达式计算
if (a == 0.0 || b == 0.0) { return a * b + c; }
当 c 为零且乘积非零时,将返回 BigDecimal 乘积的 `doubleValue()`
else { return product.doubleValue ( ); }
对于所有非零输入 a、b 和 c
else { return product.add (new BigDecimal (c)). doubleValue ( ); }
以下示例显示了使用 Open JDK9 中提供的 FMA API 执行的 `A[i]=A[i]+C*B[i]` 操作
for (int i = 0; i < A.length && i < B.length; i++) A[i] = Math.fma(C, B[i], A[i]);
单精度和双精度浮点数的 FMA 均以双精度格式计算,然后显式存储为 float 或 double 以匹配返回类型。由于双精度比 float 格式具有两倍以上的精度,因此 a*b 的乘法是精确的。将 c 加到乘积上会产生一次舍入误差。此外,由于双精度比 float 的 p 位精度高出 (2p+2) 位,因此将 a*b+c 先舍入到 double 再舍入到 float 的两次舍入,等同于将中间结果直接舍入到 float。FMA 方法调用进一步映射到 CPU FMA 指令(如果可用),以获得更高的性能。
当您在最新的 Open JDK 9 版本和源代码构建版本上运行 Java 应用程序时,JVM 会为 FMA 指令可用的硬件(从第二代 Intel Core 处理器开始的 Intel 处理器)启用基于硬件的 FMA 内在函数。JDK9 中的 `java.lang.Math.fma(a,b,c)` 方法会生成 FMA 内在函数,这些函数计算 `a*b+c` 表达式的值。
JVM 选项 –XX:+PrintIntrinsics 可用于确认 FMA 的内在化
@ 6 java.lang.Math::fma (12 bytes) (intrinsic)
FMA 在 BLAS 机器学习算法上的性能
在 Java 程序中,可以使用 Math FMA 实现计算内核,该内核可以进一步利用现代 CPU 上的 FMA 硬件扩展。在最新的 Open JDK 10 源代码构建版本上,使用 Math.fma,在 Intel Xeon Phi 处理器上 BLAS I DDOT(点积)的性能可提高高达 3.5 倍。JVM JIT 编译器将 FMA 方法调用内在化为硬件指令,然后通过自动向量化和超级字优化将其向量化为 SIMD 操作。在最新的 Open JDK 源代码构建版本上,使用 Math FMA,在 Intel Xeon Phi 处理器上 BLAS-I DAXPY 的性能可提高高达 2.3 倍。
下面显示了原始 Java DAXPY 实现
int _r = n % 4; int _n = n - _r; for (i = 0; i < _n && i < dx.length; i+=4) { dy[i +dy_off] = dy[i +dy_off] + da*dx[i +dx_off]; dy[i+1 +dy_off] = dy[i+1 +dy_off] + da*dx[i+1 +dx_off]; dy[i+2 +dy_off] = dy[i+2 +dy_off] + da*dx[i+2 +dx_off]; dy[i+3 +dy_off] = dy[i+3 +dy_off] + da*dx[i+3 +dx_off]; } for (i = _n; i < n; i++) dy[i +dy_off] = dy[i +dy_off] + da*dx[i +dx_off]
以下是 DAXPY 算法的 `Math.fma` 实现
for (i = 0; i < dx.length; i++) dy[i] = Math.fma (da, dx[i], dy[i]);
下面显示了 JVM JIT 编译器生成的机器代码。我们看到,FMA 向量 SIMD 指令是为 Java 算法生成的,并且在最新的 Open JDK 源代码构建版本上,这些指令被映射到 Intel Xeon Phi 处理器上的 SIMD vfmadd231pd 指令。
vmovdqu64 0x10(%rbx,%r13,8),%zmm2{%k1}{z} vfmadd231pd 0x10(%r10,%r13,8),%zmm1,%zmm2{%k1}{z} ;*invokestatic fma {reexecute=0 rethrow=0 return_oop=0} ; - SmallDoubleDot::daxpy@146 (line 82
Math FMA 接口的应用
Math FMA 应用程序接口在执行基本线性代数计算的程序中非常有用。例如,密集 DGEMM(双精度矩阵乘法)的内层计算内核可以使用 `Math.fma` 操作重写,如下所示
for (j = 0; j < N; j++) { for (l = 0; l < k; l++) { if (b[j + l * ldb + _b_offset] != 0.0) { temp = alpha * b[j + l * ldb + _b_offset]; for (i = 0; i < m; i++) { c[i + j * Ldc + _c_offset] = Math.fma (temp, a[i + l * lda + _a_offset], c[i + j * Ldc + _c_offset]); } } }
稀疏矩阵计算可以重写如下
for (int steps = 0; steps < NUM_STEPS; steps++) { for (int l = 0; l < ALPHA; l++) { double Total = 0.0; int rowBegin = Rows[l]; int rowEnd = Rows[l+1]; for (int j=rowBegin; j<rowEnd; j++) { Total = Math.fma (A[Cols[j],Value[j],Total); } y[l] = Total; } }
在二项式期权定价(一种常见的金融算法)中,操作 `stepsArray[k]=pdByr*stepsArray[k+1]+puByr*stepsArray[k]` 可以使用 FMA 来编写
void BinomialOptions (double[] stepsArray, int STEPS_CACHE_SIZE, double vsdt, double x, double s, int numSteps, int NUM_STEPS_ROUND, double pdByr, double puByr) { for (int j = 0; j < STEPS_CACHE_SIZE; j++) { double profit = s * Math.exp (vsdt * (2.0D * j - numSteps)) - x; stepsArray[j] = profit > 0.0D? profit: 0.0D; } for (int j = 0; j < numSteps; j++) { for (int k = 0; k < NUM_STEPS_ROUND; ++k) { stepsArray[k] = Math.fma (puByr, stepsArray[k], (pdByr * stepsArray[k+1])); } } }
提升 Java 性能
对 Java 的新增强功能使得更快速、更好的数值计算成为可能。JVM 已得到改进,可在 `Math.fma()` 的实现中使用 Intel AVX FMA 指令。这导致矩阵乘法(HPC 和 AI 应用程序的基础工作负载)的性能得到显著提高。