背景
注意到我正在处理的java程序的执行速度比预期慢,我决定修补我认为可能导致问题的代码区域 - 从一个内部调用Math.pow(x,2) for循环.与本网站上的另一个问题相反,我创建的一个简单基准测试(最后的代码)发现用x*x替换Math.pow(x,2)实际上将循环加速了近70倍:
x*x: 5.139383ms Math.pow(x, 2): 334.541166ms
请注意,我知道基准测试并不完美,而且值肯定应该用一点点盐 - 基准的目的是得到一个大概的数字.
问题
虽然基准测试给出了有趣的结果,但它没有准确地模拟我的数据,因为我的数据主要由0组成.因此,更准确的测试是运行基准测试,而不将for循环标记为可选.根据Math.pow()的javadoc
如果第一个参数为正零且第二个参数大于零,或者第一个参数为正无穷大且第二个参数小于零,则结果为正零.
所以预计这个基准测试运行得更快吧!?但实际上,这又慢了很多:
x*x: 4.3490535ms Math.pow(x, 2): 3082.1720006ms
当然,人们可能期望math.pow()代码比简单的x*x代码运行慢一点,因为它需要适用于一般情况,但速度要慢700倍?到底是怎么回事!?为什么0情况比Math.random()情况慢得多?
更新: 根据@Stephen C的建议更新代码和时间.然而,这没有什么区别.
代码用于基准测试
请注意,重新排序这两个测试可以忽略不计.
public class Test { public Test(){ int iterations = 100; double[] exampleData = new double[5000000]; double[] test1Results = new double[iterations]; double[] test2Results = new double[iterations]; //Optional for (int i = 0; i < exampleData.length; i++) { exampleData[i] = Math.random(); } for (int i = 0; i < iterations; i++) { test1Results[i] = test1(exampleData); test2Results[i] = test2(exampleData); } System.out.println("x*x: " + calculateAverage(test1Results) / 1000000 + "ms"); System.out.println("Math.pow(x, 2): " + calculateAverage(test2Results) / 1000000 + "ms"); } private long test1(double[] exampleData){ double total = 0; long startTime; long endTime; startTime = System.nanoTime(); for (int j = 0; j < exampleData.length; j++) { total += exampleData[j] * exampleData[j]; } endTime = System.nanoTime(); System.out.println(total); return endTime - startTime; } private long test2(double[] exampleData){ double total = 0; long startTime; long endTime; startTime = System.nanoTime(); for (int j = 0; j < exampleData.length; j++) { total += Math.pow(exampleData[j], 2); } endTime = System.nanoTime(); System.out.println(total); return endTime - startTime; } private double calculateAverage(double[] array){ double total = 0; for (int i = 0; i < array.length; i++) { total += array[i]; } return total/array.length; } public static void main(String[] args){ new Test(); } }
apangin.. 7
虽然这是一个糟糕的基准,但它幸运地揭示了一个有趣的效果.
这些数字表明您显然在"客户端"VM下运行基准测试.它没有非常强大的JIT编译器(称为C1编译器),缺乏许多优化.难怪它没有像人们预期的那样好.
Math.pow
即使没有副作用,客户端VM也不够智能,无法消除呼叫.
而且,它既没有专门的快速路径也Y=2
没有X=0
.至少,它直到Java 9才有.最近在JDK-8063086中修复了这个问题,然后在JDK-8132207中进一步优化.
但有趣的是,使用C1编译器Math.pow
确实更慢X=0
!
但为什么?由于实施细节.
x86体系结构不提供计算X ^ Y的硬件指令.但还有其他有用的说明:
FYL2X
计算Y*log 2X
F2XM1
计算2 ^ X - 1
由此,X ^ Y = 2 ^(Y*log 2 X).由于log 2X仅为X> 0定义FYL2X
,因此X=0
以及for 返回异常-Inf
.因此,它发生X=0
在慢速异常路径而不是专用快速路径中.
那么该怎么办?
首先,停止使用客户端VM,特别是如果您关心性能.切换到64位版本的最新JDK 8,您将获得最佳的C2优化JIT编译器.当然,它很好地处理Math.pow(x, 2)
等等.然后使用像JMH这样的适当工具编写正确的基准.
虽然这是一个糟糕的基准,但它幸运地揭示了一个有趣的效果.
这些数字表明您显然在"客户端"VM下运行基准测试.它没有非常强大的JIT编译器(称为C1编译器),缺乏许多优化.难怪它没有像人们预期的那样好.
Math.pow
即使没有副作用,客户端VM也不够智能,无法消除呼叫.
而且,它既没有专门的快速路径也Y=2
没有X=0
.至少,它直到Java 9才有.最近在JDK-8063086中修复了这个问题,然后在JDK-8132207中进一步优化.
但有趣的是,使用C1编译器Math.pow
确实更慢X=0
!
但为什么?由于实施细节.
x86体系结构不提供计算X ^ Y的硬件指令.但还有其他有用的说明:
FYL2X
计算Y*log 2X
F2XM1
计算2 ^ X - 1
由此,X ^ Y = 2 ^(Y*log 2 X).由于log 2X仅为X> 0定义FYL2X
,因此X=0
以及for 返回异常-Inf
.因此,它发生X=0
在慢速异常路径而不是专用快速路径中.
那么该怎么办?
首先,停止使用客户端VM,特别是如果您关心性能.切换到64位版本的最新JDK 8,您将获得最佳的C2优化JIT编译器.当然,它很好地处理Math.pow(x, 2)
等等.然后使用像JMH这样的适当工具编写正确的基准.