在下面的代码和结果中,我们可以看到“Traverse2”比“Traverse1”快得多,实际上它们只是遍历相同数量的元素。
1.这种差异是如何发生的?
2.在较短的交互中设置较长的交互会有更好的表现吗?
public class TraverseTest {
public static void main(String[] args)
{
int a[][] = new int[100][10];
System.out.println(System.currentTimeMillis());
//Traverse1
for(int i = 0; i < 100; i++)
{
for(int j = 0; j < 10; j++)
a[i][j] = 1;
}
System.out.println(System.currentTimeMillis());
//Traverse2
for(int i = 0; i < 10; i++)
{
for(int j = 0; j < 100; j++)
a[j][i] = 2;
}
System.out.println(System.currentTimeMillis());
}
}
结果:
1347116569345
1347116569360
1347116569360
如果我将其更改为
System.out.println(System.nanoTime());
结果将是:
4888285195629
4888285846760
4888285914219
这意味着如果我们在内部放置更长的交互将会有更好的表现。它似乎与缓存命中理论存在一些冲突。
答案 0 :(得分:2)
我怀疑你在微观基准测试中看到的结果中的任何陌生都是由于基准测试本身存在缺陷。
例如:
您的基准测试没有考虑“JVM热备”效果,例如JIT编译器不会立即编译为本机代码。 (这仅在代码执行一段时间后发生,并且JVM已经测量了一些使用数量以帮助优化。)处理此问题的正确方法是将整个批次置于运行几次的循环中,并丢弃由于预热效应而看起来“奇怪”的任何初始时间组。
基准中的循环理论上可以进行优化。 JIT编译器可能会推断他们不会做任何影响程序输出的工作。
最后,我想提醒你,像这样的手工优化通常是一个坏主意......除非你有令人信服的证据证明你的手工优化是值得的,并且这段代码真的在哪里该应用程序花费了大量时间。
答案 1 :(得分:1)
我的输出(原始代码100i / 10j vs 10i / 100j):
1347118083906
1347118083906
1347118083906
您使用非常糟糕的时间分辨率进行快速计算。
我将i和j限制都改为1000。
int a[][] = new int[1000][1000];
System.out.println(System.currentTimeMillis());
//Traverse1
for(int i = 0; i < 1000; i++)
{
for(int j = 0; j < 1000; j++)
a[i][j] = 1;
}
System.out.println(System.currentTimeMillis());
//Traverse2
for(int i = 0; i < 1000; i++)
{
for(int j = 0; j < 1000; j++)
a[j][i] = 2;
}
System.out.println(System.currentTimeMillis());
输出:
1347118210671
1347118210687 //difference is 16 ms
1347118210703 //difference is 16 ms again -_-
两种可能性:
所以我将输出更改为System.nanoTime()
int a[][] = new int[1000][1000];
System.out.println(System.nanoTime());
//Traverse1
for(int i = 0; i < 1000; i++)
{
for(int j = 0; j < 1000; j++)
a[i][j] = 1;
}
System.out.println(System.nanoTime());
//Traverse2
for(int i = 0; i < 1000; i++)
{
for(int j = 0; j < 1000; j++)
a[j][i] = 2;
}
System.out.println(System.nanoTime());
输出:
16151040043078
16151047859993 //difference is 7800000 nanoseconds
16151061346623 //difference is 13500000 nanoseconds --->this is half speed
1.这种差异是如何发生的?
请注意,即使省略了你只是使用了错误的时间分辨率,你也会做出错误的比较与不平等的情况。首先是连续访问,而第二个不是。
让我们说第一个嵌套循环只是为第二个嵌套循环做准备然后它会使你的“第二个更快”的假设更加错误。
不要忘记2D数组是java中的“数组数组”。因此,最右边的索引会显示一个连续的区域。第一版的速度更快。
2.在较短的交互中设置较长的交互会有更好的表现吗?
for(int i = 0; i < 10; i++)
{
for(int j = 0; j < 100; j++)
a[j][i] = 2;
}
增加第一个索引的速度较慢,因为下一次迭代会消耗千字节,因此您无法再使用缓存行。
绝对不是!
答案 2 :(得分:1)
首先,始终在循环中多次运行microbenchmark测试。然后,您将看到两个时间都为0,因为数组大小太小。要获得非零时间,请将数组大小增加100倍。 Traverse1的时间约为32 ms,Traverse2的时间约为250 ms。 不同之处在于处理器使用缓存。访问顺序存储器地址要快得多。
答案 3 :(得分:1)
在我看来,数组的大小也会影响结果。像:
public class TraverseTest {
public static void main(String[] args)
{
int a[][] = new int[10000][2];
System.out.println(System.currentTimeMillis());
//Traverse1
for(int i = 0; i < 10000; i++)
{
for(int j = 0; j < 2; j++)
a[i][j] = 1;
}
System.out.println(System.currentTimeMillis());
//Traverse2
for(int i = 0; i < 2; i++)
{
for(int j = 0; j < 10000; j++)
a[j][i] = 2;
}
System.out.println(System.currentTimeMillis());
}
}
Traverse1需要 10000 * 3 + 1 = 30001 比较来决定是否退出迭代, 但Traverse2只需 2 * 10001 + 1 = 20003 比较。
Traverse1需要1.5倍于Traverse2的比较次数。