背景
注意到我正在处理的java程序的执行速度比预期慢,我决定修补我认为可能导致问题的代码区域 - 调用Math.pow(x,2)来自for循环。与another questions on this site相反,我创建的一个简单基准(结尾处的代码)发现用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();
}
}
答案 0 :(得分:7)
虽然这是一个糟糕的基准,但它幸运地揭示了一个有趣的效果。
数字表明您显然在“客户端”VM下运行基准测试。它没有非常强大的JIT编译器(称为C1编译器),缺乏许多优化。难怪它没有人们期望的那么好。
Math.pow
来电。Y=2
也没有X=0
的专门快速路径。至少,它直到Java 9才有。最近在JDK-8063086修复了这个问题,然后在JDK-8132207进一步优化。 但有趣的是,Math.pow
对于使用C1编译器的X=0
确实较慢!
但为什么呢?由于实施细节。
x86架构不提供计算X ^ Y的硬件指令。但还有其他有用的说明:
FYL2X
计算Y * log 2 X F2XM1
计算2 ^ X - 1 因此,X ^ Y = 2 ^(Y * log 2 X)。由于log 2 X仅定义为X> 1。 0,FYL2X
以X=0
的例外结束并返回-Inf
。因此,X=0
在慢速异常路径中处理,而不是在专门的快速路径中处理。
那该怎么办?
首先,停止使用客户端VM,特别是如果您关心性能。切换到64位版本的最新JDK 8,您将获得最佳的C2优化JIT编译器。当然,它很好地处理Math.pow(x, 2)
等。然后使用correct benchmark之类的适当工具编写JMH。
答案 1 :(得分:4)
可能与JDK 7中的这种回归有关:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8029302
来自错误报告:
有一个Math.pow性能回归,其中2个输入的功率运行 比其他值慢。
我已经替换了所有对Math.pow()的调用,因为:
public static double pow(final double a, final double b) {
if (b == 2.0) {
return (a * a);
} else {
return Math.pow(a, b);
}
}
根据错误报告,它已在JDK 8中修复,符合上述@BartKiers评论。
答案 2 :(得分:3)
虽然@whiskeyspider发现的错误报告是相关的,但我不认为这是完整的解释。根据错误报告,回归大约减少4倍。但在这里,我们看到大约1000倍的放缓。差异太大,不容忽视。
我认为我们在这里看到的部分问题是基准本身。看看这个:
for (int j = 0; j < exampleData.length; j++) {
double output = exampleData[j] * exampleData[j];
}
正文中的语句分配给未使用的局部变量。它可以通过JIT编译器进行优化。 (事实上,整个循环可以被优化掉......虽然凭经验看似不会发生在这里。)
相比之下:
for (int j = 0; j < exampleData.length; j++) {
double output = Math.pow(exampleData[j], 2);
}
除非JIT编译器知道pow
是无副作用的,否则无法进行优化。由于pow
的实现是在本机代码中,因此必须以制作方法的方式传授这些知识&#34;内在的&#34; ......引擎盖下。从错误报告分析,&#34; intinsification&#34;不同Java版本/发行版之间是回归的根本原因。我怀疑OP的基准测试中的缺陷是放大效果。
修复是为了确保使用output
值,以便JIT编译器无法对其进行优化; e.g。
double blackhole = 0; // declared at start ...
...
for (int j = 0; j < exampleData.length; j++) {
blackhole += exampleData[j] * exampleData[j];
}
...
for (int j = 0; j < exampleData.length; j++) {
blackhole += Math.pow(exampleData[j], 2);
}
参考:https://stackoverflow.com/a/513259/139985 ...尤其是规则#6。
答案 3 :(得分:0)
使用Math类中的任何方法需要更长的时间,然后只使用一个简单的运算符(如果可能)。这是因为程序必须将Math。方法()的输入传递给Math类,然后Math类将执行操作,然后Math类将返回从Math计算的值方法()。所有这些都需要比使用*,/,+或 - 等基本运算符更多的处理能力。