Hun*_*gry 7 java math performance benchmarking pow
背景
注意到我正在处理的java程序的执行速度比预期慢,我决定修补我认为可能导致问题的代码区域 - 从一个内部调用Math.pow(x,2) for循环.与本网站上的另一个问题相反,我创建的一个简单基准测试(最后的代码)发现用x*x替换Math.pow(x,2)实际上将循环加速了近70倍:
x*x: 5.139383ms
Math.pow(x, 2): 334.541166ms
Run Code Online (Sandbox Code Playgroud)
请注意,我知道基准测试并不完美,而且值肯定应该用一点点盐 - 基准的目的是得到一个大概的数字.
问题
虽然基准测试给出了有趣的结果,但它没有准确地模拟我的数据,因为我的数据主要由0组成.因此,更准确的测试是运行基准测试,而不将for循环标记为可选.根据Math.pow()的javadoc
如果第一个参数为正零且第二个参数大于零,或者第一个参数为正无穷大且第二个参数小于零,则结果为正零.
所以预计这个基准测试运行得更快吧!?但实际上,这又慢了很多:
x*x: 4.3490535ms
Math.pow(x, 2): 3082.1720006ms
Run Code Online (Sandbox Code Playgroud)
当然,人们可能期望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();
}
}
Run Code Online (Sandbox Code Playgroud)
虽然这是一个糟糕的基准,但它幸运地揭示了一个有趣的效果.
这些数字表明您显然在"客户端"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 2XF2XM1 计算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这样的适当工具编写正确的基准.