在我的java程序中,我的for循环看起来大致如下:
ArrayList<MyObject> myList = new ArrayList<MyObject>();
putThingsInList(myList);
for (int i = 0; i < myList.size(); i++) {
doWhatsoever();
}
由于列表的大小没有变化,我试图通过用变量替换循环的终止表达式来加速循环。
我的想法是:由于ArrayList的大小在迭代时可能会发生变化,因此必须在每个循环周期执行终止表达式。如果我知道(但JVM没有),它的大小将保持不变,变量的使用可能会加快速度。
ArrayList<MyObject> myList = new ArrayList<MyObject>();
putThingsInList(myList);
int myListSize = myList.size();
for (int i = 0; i < myListSize; i++) {
doWhatsoever();
}
然而,这种解决方案速度较慢,速度较慢;同时使myListSize最终不会改变任何东西!我的意思是我能理解,如果速度根本没有改变;因为JVM刚刚发现,大小并没有改变和优化代码。但为什么它会变慢?
然而,我改写了该计划;现在列表的大小随着每个周期而变化:if i%2==0
,我删除列表的最后一个元素,否则我在列表的末尾添加一个元素。所以现在我必须在每次迭代中调用myList.size()
操作,我猜想。
我不知道这是否真的正确,但myList.size()
终止表达式仍然比仅使用一直保持不变的变量作为终止表达式更快......
任何想法为什么?
编辑(我在这里新建,我希望这是方法,怎么做) 我的整个测试程序看起来像这样:
ArrayList<Integer> myList = new ArrayList<Integer>();
for (int i = 0; i < 1000000; i++)
{
myList.add(i);
}
final long myListSize = myList.size();
long sum = 0;
long timeStarted = System.nanoTime();
for (int i = 0; i < 500; i++)
{
for (int j = 0; j < myList.size(); j++)
{
sum += j;
if (j%2==0)
{
myList.add(999999);
}
else
{
myList.remove(999999);
}
}
}
long timeNeeded = (System.nanoTime() - timeStarted)/1000000;
System.out.println(timeNeeded);
System.out.println(sum);
发布代码的性能(平均10次执行): 4102ms for myList.size() myListSize的4230ms
没有if-then-else语句(因此使用常量myList大小) myList.size()的172ms myListSize为329ms
因此两种版本的速度差异仍然存在。在带有if-then-else部分的版本中,百分比差异当然较小,因为大量时间用于添加和删除列表操作。
答案 0 :(得分:4)
问题在于这一行:
final long myListSize = myList.size();
将此更改为int
并且看,运行时间相同。为什么?由于每次迭代都需要int
与long
进行比较,因此需要对int
进行扩展转换,这需要时间。
请注意,编译和优化代码时,差异也很大(但可能不完全)消失,从以下JMH基准测试结果可以看出:
# JMH 1.11.2 (released 7 days ago)
# VM version: JDK 1.8.0_51, VM 25.51-b03
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
...
# Run complete. Total time: 00:02:01
Benchmark Mode Cnt Score Error Units
MyBenchmark.testIntLocalVariable thrpt 20 81892.018 ± 734.621 ops/s
MyBenchmark.testLongLocalVariable thrpt 20 74180.774 ± 1289.338 ops/s
MyBenchmark.testMethodInvocation thrpt 20 82732.317 ± 749.430 ops/s
以下是它的基准代码:
public class MyBenchmark {
@State( Scope.Benchmark)
public static class Values {
private final ArrayList<Double> values;
public Values() {
this.values = new ArrayList<Double>(10000);
for (int i = 0; i < 10000; i++) {
this.values.add(Math.random());
}
}
}
@Benchmark
public double testMethodInvocation(Values v) {
double sum = 0;
for (int i = 0; i < v.values.size(); i++) {
sum += v.values.get(i);
}
return sum;
}
@Benchmark
public double testIntLocalVariable(Values v) {
double sum = 0;
int max = v.values.size();
for (int i = 0; i < max; i++) {
sum += v.values.get(i);
}
return sum;
}
@Benchmark
public double testLongLocalVariable(Values v) {
double sum = 0;
long max = v.values.size();
for (int i = 0; i < max; i++) {
sum += v.values.get(i);
}
return sum;
}
}
<强> P.S:强>
我的想法是:由于ArrayList的大小可能会改变 迭代它,终止表达式必须在每个循环中执行 周期。如果我知道(但JVM没有),它的大小将保持不变 常量,变量的使用可能会加快速度。
您的假设是错误的,原因有两个:首先,VM可以轻松确定通过转义分析,myList
中存储的列表不会转义该方法(因此可以在堆栈中自由分配它)例子)。
更重要的是,即使列表是在多个线程之间共享的,因此在我们运行循环时可能会从外部进行修改,在没有任何同步的情况下,运行我们的循环的线程完全有效。根本没有发生变化。
答案 1 :(得分:2)
与往常一样,事情并非总是如此......
首先,ArrayList.size()
不会在每次调用时重新计算,只有在调用适当的mutator时才会重新计算。所以经常打电话很便宜。
这些循环中哪一个最快?
// array1 and array2 are the same size.
int sum;
for (int i = 0; i < array1.length; i++) {
sum += array1[i];
}
for (int i = 0; i < array2.length; i++) {
sum += array2[i];
}
或
int sum;
for (int i = 0; i < array1.length; i++) {
sum += array1[i];
sum += array2[i];
}
本能地,你会说第二个循环是最快的,因为它不会迭代两次。但是,一些优化实际上会导致第一个循环最快,具体取决于内存行走步幅导致大量内存缓存未命中。
旁注:这种编译器优化技术称为循环 干扰
这个循环:
int sum;
for (int i = 0; i < 1000000; i++) {
sum += list.get(i);
}
与:
不同// Assume that list.size() == 1000000
int sum;
for (int i = 0; i < list.size(); i++) {
sum += list.get(i);
}
在第一种情况下,编译绝对知道它必须迭代一百万次并将常量放在常量池中,因此可以进行某些优化。
更接近的等价物是:
int sum;
final int listSize = list.size();
for (int i = 0; i < listSize; i++) {
sum += list.get(i);
}
但只有在JVM知道listSize
的值是什么之后。 final
关键字为编译器/运行时提供了可以利用的某些保证。如果循环运行的时间足够长,则JIT编译将启动,从而使执行更快。
答案 2 :(得分:0)
因为这引起了我的兴趣,所以我决定快速测试一下:
public class fortest {
public static void main(String[] args) {
long mean = 0;
for (int cnt = 0; cnt < 100000; cnt++) {
if (mean > 0)
mean /= 2;
ArrayList<String> myList = new ArrayList<String>();
putThingsInList(myList);
long start = System.nanoTime();
int myListSize = myList.size();
for (int i = 0; i < myListSize; i++) doWhatsoever(i, myList);
long end = System.nanoTime();
mean += end - start;
}
System.out.println("Mean exec: " + mean/2);
}
private static void doWhatsoever(int i, ArrayList<String> myList) {
if (i % 2 == 0)
myList.set(i, "0");
}
private static void putThingsInList(ArrayList<String> myList) {
for (int i = 0; i < 1000; i++) myList.add(String.valueOf(i));
}
}
我没有看到你所看到的那种行为。
使用myList.size() 1800ns 表示超过100000次迭代的执行时间 with myListSize 因此,我怀疑你的代码是由错误的函数执行的。在上面的示例中,如果只填充ArrayList
一次,有时可以看到更快的执行,因为doWhatsoever()
只会在第一个循环上执行某些操作。我怀疑其余部分正在被优化掉,因此大大减少了执行时间。你可能有类似的情况,但是没有看到你的代码就可能几乎不可能找出那个。
答案 3 :(得分:0)
还有另一种方法可以加速每个循环使用的代码
ArrayList<MyObject> myList = new ArrayList<MyObject>();
putThingsInList(myList);
for (MyObject ob: myList) {
doWhatsoever();
}
但我同意@ showp1984,其他一些部分正在减慢代码。