作者:姜逸坤
1. 起初
最近在进行ARM切换的过程中发现了很多因为Java Math库在不同的平台上的精度不同导致用例失败,我们以Math.log为例,做一下简单的分析。下面是一个简单的计算log(3)的示例:
| 1 | public class Hello { | 
我们发现,在x86下,Math的结果为1.0986122886681098。
| 1 | on x86 | 
而aarch64的结果为1.0986122886681096。
| 1 | on aarch64 | 
而在Java 8的官方文档中,对此有明确说明:
Unlike some of the numeric methods of class StrictMath, all implementations of the equivalent functions of class Math are not defined to return the bit-for-bit same results. This relaxation permits better-performing implementations where strict reproducibility is not required.
因此,结论是:Math的结果有可能是不精确的,如果结果对精度有苛求,那么请使用StrictMath。
在此,我们留下2个疑问:
- 为什么说Math的实现不是the bit-for-bit same results?
- Math是怎么实现在各个架构下better-performing implementations的?
2. 深度探索一下Math的实现
为了能够更清晰的看到StrictMath的实现,我们深入的看了下JDK的实现。
2.1 Math和StrictMath的基本实现
我们从Math.log和StrictMath.log的实现为例,进行深入学习:
- Math.log的代码表面上很简单,就是直接调用StrictMath.log。1 
 2
 3public static double log(double a) { 
 return StrictMath.log(a); // default impl. delegates to StrictMath
 }
- StrictMath的代码,会调用StrictMath.c中的方法,最终会调用fdlibm的e_log.c的实现。
总体的实现和下图类似:
对于StrictMath来说,没有什么黑科技,最终的实现就是e_log.c的ieee754标准实现,是通过C语言实现的,所以在各个平台的表现是一样的,整个流程如图中蓝色部分。感兴趣的同学可以看e_log.c的源码实现即可。
2.2 Math的黑科技
回到我们最初的起点,再加上一个问题:
- 为什么说Math的实现不是the bit-for-bit same results?
- Math是怎么实现在各个架构下better-performing implementations的?
- 既然Math的实现,也是直接调用StrictMath,为什么结果确不一样呢?
原来,JVM为了让各个arch的CPU能够充分的发挥自己CPU的优势,会根据架构不同,会通过Hotspot intrinsics替换掉Math函数的实现,我们可以从代码vmSymbols.hpp看到,Math的很多实现都被替换掉了。log的替换类似于:
| 1 | do_intrinsic(_dlog, java_lang_Math, log_name, double_double_signature, F_S) | 
最终,Math的调用为下图红色部分:

log的实现:
- 在x86下,最终其实调用的是assembler_x86.cpp中的flog实现:1 
 2
 3
 4
 5void Assembler::flog() { 
 fldln2();
 fxch();
 fyl2x();
 }
- 而在aarch64下,我们可以从src/hotspot/cpu/目录下看到,aarch64并未实现优化版本。因此,实际aarch64调用的就是标准的StrictMath。
正因如此,x86汇编的计算结果的差异导致了x86和aarch64结果在Math.log差异。
当然,aarch64也在JDK 11中,对部分的Math接口做了加速实现,有兴趣可以看看JEP 315: Improve Aarch64 Intrinsics的实现。
3. toRadians的小插曲
在ARM优化过程中,有的是因为Math库和StrictMath不同的实现造成结果不同,所以我们如果对精度要求非常高,直接切到StrictMath即可。
但有的函数,由于在Java大版本升级的过程中,出现了一些实现的差异,先看一个简单的Java程序
| 1 | public class Hello { | 
我们分别看看在Java11和Java8的结果:
| 1 | $ /usr/lib/jvm/java-11-openjdk-amd64/bin/java Hello | 
| 1 | $ /usr/lib/jvm/java-1.8.0-openjdk-amd64/bin/java Hello | 
最后一位很奇怪的差了1,我们继续深入进去看到toRadians的实现:
- Java8的实现为:1 
 2
 3
 4// Java 8 
 public static double toDegrees(double angrad) {
 return angrad * 180.0 / PI;
 }
- Java11的实现为:原来在Java11的实现中,为了优化性能,将1 
 2
 3
 4private static final double DEGREES_TO_RADIANS = 0.017453292519943295; 
 public static double toRadians(double angdeg) {
 return angdeg * DEGREES_TO_RADIANS;
 }* 180.0 / PI提前算好了,这样每次只用乘以乘数即可,从而化简了计算。这也最终导致了,Java8和Java11在精度上有一些差别。
4. 总结
- Math在各个arch下的实现不同,精度也不同,如果对精度要求很高,可以使用StrictMath。
- Java不同版本的优化,也有可能导致Math库的精度不同
- Math库在实现时,利用intrinsics机制,把各个arch下Math的实现换掉了,从而充分的发挥各个CPU自身的优势。
