我用Java编写了一个程序来计算像1百万的极端因子。它本质上是从1到n
和每次迭代开始循环,将BigDecimal
与循环中的计数器变量的值相乘。循环完成后,它会调用BigDecimal#toPlainString()
,它返回作为String生成的数字。然而,这种方法的调用需要非常长的时间才能执行。例如,在下面的代码中:
public static void main(String[] args) {
BigDecimal number = new BigDecimal(1);
long startTime = System.currentTimeMillis();
for (int i = 1; i < 500000; i++) {
number = number.multiply(new BigDecimal(i));
}
System.out.println("Generating took: " + (System.currentTimeMillis() - startTime) + "ms. Creating String.");
startTime = System.currentTimeMillis();
String result = number.toPlainString();
System.out.println("String generation took: " + (System.currentTimeMillis() - startTime) + "ms");
FileUtils.writeStringToFile(new File("Path/To/File"), result);
}
控制台的输出是:
Generating took: 181324ms. Creating String.
String generation took: 710498ms
表示方法toPlainString()
花了多长时间。
我知道我们正在处理大量数字(在上面的例子中是数字的数字),但我想知道是否有任何方法可以加速这种方法,我应该怎么做呢?
谢谢!
编辑#1 :在帖子中添加的毫秒时间计算的唯一原因是为了将'long'带入预期,并可能展示其行为代码,以防所有读者无法重现问题。我要做的是确定为什么在我的情况下花了这么长时间,最重要的是如何加快转换为String的过程。
答案 0 :(得分:4)
BigDecimal#PlainString
使用Java 7生成字符串需要很长时间的原因是:它在Java 7中实现效率非常低。幸运的是,它在Java 8中的运行时间更快 <。 / p>
在这里,重要的是要注意在这种特殊情况下,它实际上不是BigDecimal
中的字符串创建,而是BigInteger
中的字符串创建。在给定示例中计算的值是大因子,因此实际上是积分值。然后,scale
的内部BigDecimal
字段将为0
,并且查看toPlainString
方法会显示内部{{1}的字符串值1}}字段将被返回:
intVal
此public String toPlainString() {
if(scale==0) {
if(intCompact!=INFLATED) {
return Long.toString(intCompact);
} else {
return intVal.toString();
}
}
...
}
字段为intVal
,这是真正的罪魁祸首。
以下程序不打算作为正确的&#34; microbenchmark&#34;,但只能估算性能:它会创建几个阶乘,并生成字符串表示形式这些:
BigInteger
使用Java 7(u07),在我的(旧)PC上,输出沿着
行import java.math.BigDecimal;
public class BigDecimalToPlainStringPerformance
{
public static void main(String[] args)
{
for (int n = 10000; n <= 50000; n += 5000)
{
BigDecimal number = factorial(n);
long before = System.nanoTime();
String result = number.toPlainString();
long after = System.nanoTime();
double ms = (after - before) / 1e6;
System.out.println(n + "! took " + ms +
" ms, length " + result.length());
}
}
private static BigDecimal factorial(int n)
{
BigDecimal number = new BigDecimal(1);
for (int i = 1; i < n; i++)
{
number = number.multiply(new BigDecimal(i));
}
return number;
}
}
幸运的是,这个性能问题已在Java 8中得到修复。对于Java 8(u45),输出是
10000! took 514.98249 ms, length 35656
15000! took 1232.86507 ms, length 56126
20000! took 2364.799995 ms, length 77333
25000! took 3877.565724 ms, length 99090
30000! took 5814.925361 ms, length 121283
35000! took 8231.13608 ms, length 143841
40000! took 11088.823021 ms, length 166709
45000! took 14344.778177 ms, length 189850
50000! took 18155.089823 ms, length 213232
显示性能得到了显着提升显着。
从快速浏览OpenJDK中的提交日志,有一个提交可能是最相关的:
Accelerate conversion to string by means of Schoenhage recursive base conversion
(我没有验证这个,但它似乎是唯一一个致力于提高10000! took 77.20227 ms, length 35656
15000! took 113.811951 ms, length 56126
20000! took 188.293764 ms, length 77333
25000! took 261.328745 ms, length 99090
30000! took 355.001264 ms, length 121283
35000! took 481.912925 ms, length 143841
40000! took 610.812827 ms, length 166709
45000! took 698.80725 ms, length 189850
50000! took 840.87391 ms, length 213232
性能的
答案 1 :(得分:2)
首先,您的基准测试不可重复。在你的情况下,阶乘部分需要181324ms而字符串生成需要710498ms,所以字符串生成为710498/181324 = 3.9倍慢作为阶乘部分。
当我像你写的那样运行一次时,它会给出这些结果。
Generating took: 90664ms. Creating String.
String generation took: 3465ms
因此字符串生成为90644/3465 = 26倍于阶乘部分。
运行基准测试时,需要多次运行才能获得平均值。特别是当你像你一样运行长时间运行的微基准时,因为你的计算机可能同时发生了许多其他事情 - 也许你的病毒检查程序被踢了,或者你的Java进程由于内存不足而被换成磁盘,或Java决定进行完整的垃圾收集。
其次,你没有预热VM,所以目前还不清楚你的基准测试。您是否对HotSpot编译引擎的本机编译器或实际代码进行基准测试?这就是为什么你总是需要在运行像你这样的微基准测试之前预热VM。
最好的方法是使用适当的微基准测试框架。另一种方法是运行你的代码更多次(使用for循环),当它停止在不再减少的时间时,你有一个很好的迹象表明预热已经完成,你可以采取下一对的平均值跑来想出一个数字。
在我的MacBookPro上运行,这导致阶乘部分的平均值为80144毫秒,字符串生成的平均值为2839毫秒(注意我还没有查看内存使用情况)。
因此字符串生成是80144/2839 = 28倍于阶乘部分。
如果您可以在计算机上多次重现相同的结果,而在运行程序时根本没有触摸它,并且您的计算机有足够的内存,那么就会发生一些有趣的事情。但问题不在于BigDecimal中的toPlainString()
方法 - 该方法比代码的阶乘部分快得多。