我一直在测试cpu缓存优化,我做的一个简单测试就是在嵌套循环中求和2048x2048整数矩阵,首先我尝试使用顺序索引(总是跳转到下一个内存整数),然后使用长宽度内存访问(跳到第2048个下一个整数),我每次运行这些代码1000次,然后我得到每个代码的平均执行时间,测试是在Intel Core 2 Duo E4600 2.4 GHz上执行的,没有其他进程可以在背景上损害执行时间,结果如下:
顺序索引:平均值:8ms。
长宽度内存访问:平均值:43。
以下是源代码:
import java.util.Arrays;
import java.util.Random;
public class Main {
public static void main(String[] args) {
int [][] matrix = new int[2048][2048];
long tInitial = 0;
long tFinal = 0;
long [] avgExecTime = new long[1000];
for (int i = 0; i < avgExecTime.length; i++) {
fillWithRandomNumbers(matrix);
tInitial = System.currentTimeMillis();
sumSequentially(matrix);
tFinal = System.currentTimeMillis();
avgExecTime[i] = tFinal - tInitial;
}
System.out.println(Arrays.stream(avgExecTime).sum() / avgExecTime.length);
for (int i = 0; i < avgExecTime.length; i++) {
fillWithRandomNumbers(matrix);
tInitial = System.currentTimeMillis();
sumRandomly(matrix);
tFinal = System.currentTimeMillis();
avgExecTime[i] = tFinal - tInitial;
}
System.out.println(Arrays.stream(avgExecTime).sum() / avgExecTime.length);
}
public static void fillWithRandomNumbers(int [][] matrix) {
Random r = new Random();
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
matrix[i][j] = r.nextInt();
}
}
}
public static long sumSequentially(int [][] matrix) {
long total = 0;
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
total += matrix[i][j];
}
}
return total;
}
public static long sumRandomly(int [][] matrix) {
long total = 0;
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
total += matrix[j][i];
}
}
return total;
}
}
我的问题是:
我已经尝试过不使用方法并在main方法上执行所有操作,结果几乎相同。
答案 0 :(得分:2)
javac
不会优化接近这个程度的任何地方,而JIT可能会。这使得JVM在某些方面具有优势,因为优化可以针对在运行时观察到的特定行为以及运行JVM的处理器的特定功能和怪癖进行定制。
您也在假设Java虚拟机及其对数组的处理,尤其是嵌套数组。虽然C和/或Fortran等语言处理具有矩形数据结构的多维数组,但Java不会(因此允许“不规则数组”,即int [] [],arr[0].length
不需要等于{ {1}}。
这是使用对象引用的数组实现的,其中每个对象引用都指向您的arr[1].length
。显然,Java确实会进行检查,如果在紧密循环中访问单个数组,这似乎更容易检测和优化,而不是多个数组上的相同索引。
答案 1 :(得分:1)
正如您在hexafraction的回答中所看到的,您需要的优化类型是不可行的。无论如何,我认为java编译器正在进行一种微小的优化,这是因为时间不同。
我发布这个答案只是为了让这些术语变得更加明显,因为我认为它们很重要。
在Java语言中,所有被调用的非原始变量都是引用,这意味着数据不能直接访问,并且可能不是按顺序存储的。
您的数据类型很长,这是原始的,但您将它们存储到数组中,这不是原始的。当您尝试访问类似matrix[i][j]
的数组值时,JVM必须至少执行三次操作。找到matrix
对象的引用,找到对[i]
项的引用,然后找到对值[j]
的引用。
正如我所说,这些值可能不会按顺序存储,所以在大多数情况下,缓存不会太多。查看代码,我们可以看到对象的两种不同形式的访问,matrix[i][j]
和matrix[j][i]
始终i
具有外部循环索引,j
内部循环的索引。在这种情况下,我们可以对第一种情况进行简单的优化,因为第一个数组访问的值将在所有j
循环中返回相同的值,因此编译器可以看到并说,&#34;等待那里!我不需要在每个循环中找到相同的结果。我会缓存这个!&#34;,所以你的代码将评估为:
public static long sumSequentially(int [][] matrix) {
long total = 0;
for (int i = 0; i < matrix.length; i++) {
long[matrix[i].length] matrixi = matrix[i];
for (int j = 0; j < matrixi.length; j++) {
total += matrixi[j];
}
}
return total;
}
关于第二种方法,这不是真的。 j
索引会更改每个内部循环,随之改变所有循环中的第一个和第二个数组访问结果。
我在C中做了这个例子,因为C是CPU缓存的有效场景,我相信这是你的主要疑问。
结果是:
顺序求和平均值:2ms。
随机平均值:39毫秒。
所以,我们的情景几乎相同。 C将数据按顺序存储在数组中,我们使用多维矩阵(将char[2][2]
视为类型):
+--------------+------------+
| Ram address | Item index |
+--------------+------------+
| 00000120000 | [0][0] |
+--------------+------------+
| 00000120001 | [0][1] |
+--------------+------------+
| 00000120002 | [1][0] |
+--------------+------------+
| 00000120003 | [1][1] |
+--------------+------------+
CPU缓存数据和指令,不需要返回RAM来获取它们。当我们按顺序访问数据时,CPU使用我们获得的数据旁边的数据填充缓存,然后当我们访问一个时,他将缓存下一个N值,问题是当我们必须访问N + 1值时,因此缓存需要进行验证,并且会有一批数据从RAM中移出。
以第二种方式访问数据时,您将跳转各种RAM地址然后再返回。几乎你所做的每一次跳转都会使缓存失效,缓存将再次被填充,因此你需要更多的操作,而且过程会变慢。如果你想了解更多,我前段时间看到Gallery of Processor cache effects。
两种语言都有类似的行为,但出于不同的原因,我猜这是因为你感到困惑。