【问题标题】:Is javac failing on optimize my code?javac 优化我的代码失败了吗?
【发布时间】:2015-12-22 16:43:07
【问题描述】:

我一直在测试 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;
    }
}

我的问题是:

  1. 考虑到 sumRandomly 和 sumSequentially 最终结果相同,为什么 java 编译器没有优化 sumRandomly 方法?
  2. Java 编译器是否会考虑在求和发生时可能存在一个线程更改我的矩阵参数值并可能改变结果?

我已经试过不使用方法,在main方法上做所有事情,结果几乎一样。

【问题讨论】:

  • 如果数组是锯齿状的,这两种方法是不等价的。

标签: java javac jit cpu-cache


【解决方案1】:

javac 不会优化接近这个程度的任何地方,而 JIT 可以。这使 JVM 在某些方面具有优势,因为可以根据运行时观察到的特定行为以及运行 JVM 的处理器的特定功能和怪癖来定制优化。

您还对 Java 虚拟机及其对数组(尤其是嵌套数组)的处理做出了假设。虽然诸如 C 和/或 Fortran 之类的语言处理具有矩形数据结构的多维数组,但 Java 不能(因此允许“不规则数组”,即对于 int[][],arr[0].length 不必等于 arr[1].length .

这是使用对象引用数组实现的,在您的情况下,每个对象引用都指向int[]。显然,Java 会进行边界检查,如果 单个 数组在紧密循环中被访问,这似乎更容易检测和优化,而不是多个数组上的相同索引。

【讨论】:

  • 我知道 javac 不会进行优化,将其留给 jit,但最奇怪的是,即使我在 main 方法中执行所有操作并在循环中多次运行代码以生成 JVM意识到最终结果没有区别,优化似乎没有发生,无论如何谢谢。
  • @BrunoSimasHadlich 通常不会以这种方式迭代数组,而且这种优化可能需要大量的特定代码来检测,考虑到编写成本,这并没有提供切实的好处并在 JIT 内部维护它。
  • @hexafraction 如果你是对的,不应该是 matrix[i][j] 等于 matrix[j][i] 因为你正在制作 i++j++ 并且在这两种情况下你都会得到两个数组引用?事实是,外部变量只在内部循环结束时发生变化,编译器可以在第一种情况下保存第一个引用。在第二种情况下,对于每个内部循环,访问变量都会更改,并且您无法存储第一个数组的引用,这是因为顺序访问更快,您不必在每个循环中调用第一个数组访问函数,我猜。
  • @JeanJung 是的,您关于顺序访问的陈述与我的回答和期望一致。 matrix[i][j] 和 matrix[j][i] 不同的原因只是迭代顺序。
【解决方案2】:

正如您在 hexafraction 的回答中看到的那样,您需要的优化类型是不可行的。无论如何,我相信 java 编译器正在采取一个微小的优化,那是因为时代不同。

我发布这个答案只是为了让这些术语变得更加明显,因为我认为它们很重要。

数组访问函数

在Java语言中,所有调用的非原始变量都是引用,这意味着数据不能直接访问,也可能不是顺序存储的。

您的数据类型很长,是原始的,但是您将它们存储到一个数组中,这不是原始的。当您尝试访问像matrix[i][j] 这样的数组的值时,JVM 将必须执行至少三个操作。找到matrix 对象的引用,找到[i] 项的引用,最后找到你的值[j] 的引用。

正如我所说,这些值可能不会按顺序存储,因此在大多数情况下,缓存不会有太大帮助。查看您的代码,我们可以看到对对象的两种不同形式的访问,matrix[i][j]matrix[j][i] 总是带有外部循环索引的 ij 内部循环的索引。在这种情况下,我们可以对第一种情况进行简单的优化,因为第一个数组访问的值将在所有 j 循环中返回相同的值,所以编译器可以看到这个并说:“等等!我不知道”不需要每次循环都找到相同的结果。我会缓存这个!”,所以你的代码将评估为:

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 索引会更改每个内部循环,并与他一起更改所有循环中的第一个和第二个数组访问结果。

cpu 缓存

我在 C 中做了这个例子,因为 C 是 CPU 缓存的有效场景,我相信这是你的主要疑问。

结果是:

顺序求和平均:2ms。

随机求和平均:39ms。

所以,我们有几乎相同的场景。 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

这两种语言都有相似的行为,但出于不同的原因,我猜那是因为你感到困惑。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-04-10
    • 1970-01-01
    • 1970-01-01
    • 2023-04-08
    • 1970-01-01
    • 1970-01-01
    • 2014-01-22
    • 1970-01-01
    相关资源
    最近更新 更多