我用Java编写了一个程序来计算像1毫安那样的极限阶乘。本质上,它所做的就是从1开始循环到n
,每次迭代,用循环中的计数器变量的值乘以一个BigDecimal
。循环完成后,它将调用BigDecimal#toPlainString()
,它将返回作为字符串产生的数字。然而,这个方法的调用需要很长时间才能执行。例如,在下面代码中:
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()
方法所用的时间。
我知道我们处理的是巨大的数字(在上面的例子中大约有一个百万位数字),但是我想知道是否有任何方法来加速这个方法,我应该如何去做呢?
谢谢!
在文章中添加的毫秒时间计算的唯一原因是将“long”引入到“#1”中,并可能演示代码的行为,以防所有读者无法复制该问题。我想要做的是确定为什么在我的情况下花了这么长时间,最重要的是如何加快转换为字符串的过程。
发布于 2017-01-21 15:10:49
BigDecimal#PlainString
用Java 7生成字符串花费很长时间的原因是:它在Java 7中实现效率很低,幸运的是,它在Java 8中实现得更快。
在这里,可能需要注意的是,在这个特殊情况下,----实际上不是BigDecimal
中的字符串创建,而是BigInteger
中的字符串创建。在给定的示例中计算的值是一个大的阶乘,因此实际上是一个积分值。然后,scale
的内部BigDecimal
字段将是0
,查看toPlainString
方法就会发现,在本例中,将返回内部intVal
字段的字符串值:
public String toPlainString() {
if(scale==0) {
if(intCompact!=INFLATED) {
return Long.toString(intCompact);
} else {
return intVal.toString();
}
}
...
}
这个intVal
字段是一个BigInteger
,这是真正的罪魁祸首。
下面的程序是,而不是作为适当的“微基准”的,但是应该只给出性能的估计:它创建了几个阶乘,并生成了它们的字符串表示:
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 7 (u07),在我的(旧的) PC上,输出遵循
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
幸运的是,这个性能问题已经在Java 8中得到了解决。
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
表明性能有了很大的提高。
通过快速浏览OpenJDK中的提交日志,这里有一个提交可能是最相关的:
(我没有验证这一点,但似乎是唯一致力于提高toString
性能的)
发布于 2017-01-20 01:53:36
首先,您的基准是不可复制的。在您的示例中,阶乘部分占用181324ms,字符串生成占用710498 as,因此字符串生成是阶乘部分的710498/ 181324 = 3.9倍。
当我有一次像你写的那样运行它时,它给出了这些结果。
Generating took: 90664ms. Creating String.
String generation took: 3465ms
因此,字符串的生成速度是阶乘部分的26倍。
在运行基准测试时,需要多次运行它才能获得平均值。特别是当您执行像您这样的长期运行的微基准测试时,因为您的计算机中可能同时发生了许多其他事情--可能您的病毒检查器启动了,或者您的Java进程由于内存不足而被替换到磁盘,或者Java决定进行完全的垃圾收集。
其次,您没有对VM进行热身,所以还不清楚您在做什么基准测试。您是在基准测试HotSpot编译引擎的本机编译器还是实际代码?这就是为什么在运行像您这样的微基准之前,总是需要预热VM的原因。
要做到这一点,最好的方法是使用适当的微基准框架。另一种选择是运行代码更多次(使用for循环),当它确定不再减少的时间时,您有一个很好的迹象表明热身已经完成,您可以使用接下来几次运行的平均值来得出一个数字。
在我的MacBookPro上运行它,阶乘部分的平均内存为80144 ms,字符串生成的内存平均为2839 ms (注意,我还没有研究内存的使用情况)。
因此,字符串的生成速度是阶乘部分的28倍,80144 / 2839。
如果您可以在您的机器上多次重复相同的结果,而您在运行程序时根本没有接触到它,并且您的机器有足够的内存,那么就会有一些有趣的事情发生。但问题不在BigDecimal中的BigDecimal方法中--该方法比代码的阶乘部分要快得多。
https://stackoverflow.com/questions/41754262
复制相似问题