Java 2D阵列填充 - 无辜的优化导致可怕的减速

时间:2014-02-07 23:06:20

标签: java arrays performance multidimensional-array benchmarking

我尝试通过计算两个元素的每个和,相对于主对角线相反,优化每个元素的索引和的方形二维Java数组的填充。但是,除了加速或至少相当的性能之外,我还有 23(!)倍慢代码。

我的代码:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(ArrayFill.N * ArrayFill.N)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ArrayFill {
    public static final int N = 8189;
    public int[][] g;

    @Setup
    public void setup() { g = new int[N][N]; }

    @GenerateMicroBenchmark
    public int simple(ArrayFill state) {
        int[][] g = state.g;
        for(int i = 0; i < g.length; i++) {
            for(int j = 0; j < g[i].length; j++) {
                g[i][j] = i + j;
            }
        }
        return g[g.length - 1][g[g.length - 1].length - 1];
    }

    @GenerateMicroBenchmark
    public int optimized(ArrayFill state) {
        int[][] g = state.g;
        for(int i = 0; i < g.length; i++) {
            for(int j = 0; j <= i; j++) {
                g[j][i] = g[i][j] = i + j;
            }
        }
        return g[g.length - 1][g[g.length - 1].length - 1];
    }
}

基准测试结果:

Benchmark               Mode     Mean   Mean error    Units
ArrayFill.simple        avgt    0.907        0.008    ns/op
ArrayFill.optimized     avgt   21.188        0.049    ns/op


问题:
如何解释如此巨大的性能下降?

<子> P上。 S. Java版本是1.8.0-ea-b124,64位3.2 GHz AMD处理器,基准测试在单个线程中执行。

4 个答案:

答案 0 :(得分:13)

旁注:您的“优化”版本可能不会更快,即使我们将所有可能的问题都抛在一边。现代CPU中有多种资源,其中一种资源可能会阻止您进行任何改进。我的意思是:速度可能是内存限制的,并且尝试在一次迭代中写入两倍的速度可能根本没有改变。

我可以看到三个可能的原因:

  • 您的访问模式可能会强制执行绑定检查。在“简单”循环中,只有当数组是正方形时,它们才能在“优化”中明显消除。它是,但这个信息只在方法之外可用(而且一段不同的代码可以改变它!)。

  • “优化”循环中的内存位置很糟糕。它访问基本上随机的内存位置,因为在Java中没有像2D数组那样(只有new int[N][N]是快捷方式的数组数组)。 在按列迭代时,每个加载的高速缓存行只使用一个int,即64个中的4个字节。

  • 内存预取器可能会对您的访问模式产生问题。具有8189 * 8189 * 4字节的数组太大,无法容纳在任何缓存中。现代CPU具有预取器,当它发现常规访问模式时,允许预先加载高速缓存行。 prefetchers的功能差异很大。这可能与此无关,因为您只是在写,但我不确定是否可以写入尚未提取的缓存行。

我猜内存位置是罪魁祸首:

我添加了一个方法“reverse”,它的工作方式很简单,但是用

g[j][i] = i + j;

而不是

g[i][j] = i + j;

这种“无害”的变化是一场表演灾难:

Benchmark                                Mode   Samples         Mean   Mean error    Units
o.o.j.s.ArrayFillBenchmark.optimized     avgt        20       10.484        0.048    ns/op
o.o.j.s.ArrayFillBenchmark.reversed      avgt        20       20.989        0.294    ns/op
o.o.j.s.ArrayFillBenchmark.simple        avgt        20        0.693        0.003    ns/op

答案 1 :(得分:1)

我编写的版本比&#34;简单&#34;工作得更快。但是,我不知道为什么它更快(。这是代码:

class A {
  public static void main(String[] args) {
    int n = 8009;

    long st, en;

    // one
    int gg[][] = new int[n][n];
    st = System.nanoTime();
    for(int i = 0; i < n; i++) {
      for(int j = 0; j < n; j++) {
        gg[i][j] = i + j; 
      }
    }
    en = System.nanoTime();

    System.out.println("\nOne time " + (en - st)/1000000.d + " msc");

    // two
    int g[][] = new int[n][n];
    st = System.nanoTime();
    int odd = (n%2), l=n-odd;
    for(int i = 0; i < l; ++i) {
      int t0, t1;   
      int a0[] = g[t0 = i];
      int a1[] = g[t1 = ++i];
      for(int j = 0; j < n; ++j) {
        a0[j] = t0 + j;
        a1[j] = t1 + j;
      }
    }
    if(odd != 0)
    {
      int i = n-1;
      int a[] = g[i];
      for(int j = 0; j < n; ++j) {
        a[j] = i + j;
      }
    }
    en = System.nanoTime();
    System.out.println("\nOptimized time " + (en - st)/1000000.d + " msc");

    int r = g[0][0]
    //  + gg[0][0]
    ;
    System.out.println("\nZZZZ = " + r);

  }
}

结果是:

One time 165.177848 msc

Optimized time 99.536178 msc

ZZZZ = 0

有人能解释我为什么它更快?

答案 2 :(得分:1)

http://www.learn-java-tutorial.com/Arrays.cfm#Multidimensional-Arrays-in-Memory

图片:http://www.learn-java-tutorial.com/images/4715/Arrays03.gif

int [] [] ===值数组数组

int [] ===值数组

class A {
    public static void main(String[] args) {
        int n = 5000;

        int g[][] = new int[n][n];
        long st, en;

        // one
        st = System.nanoTime();
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                g[i][j] = 10; 
            }
        }
        en = System.nanoTime();
        System.out.println("\nTwo time " + (en - st)/1000000.d + " msc");

        // two
        st = System.nanoTime();
        for(int i = 0; i < n; i++) {
            g[i][i] =  20;
            for(int j = 0; j < i; j++) {
                g[j][i] = g[i][j] = 20; 
            }
        }
        en = System.nanoTime();
        System.out.println("\nTwo time " + (en - st)/1000000.d + " msc");

        // 3
        int arrLen = n*n;
        int[] arr = new int[arrLen];
        st = System.nanoTime();
        for(int i : arr) {
            arr[i] = 30;
        }
        en = System.nanoTime();
        System.out.println("\n3   time " + (en - st)/1000000.d + " msc");

        // 4
        st = System.nanoTime();
        int i, j;
        for(i = 0; i < n; i++) {
            for(j = 0; j < n; j++) {
                arr[i*n+j] = 40;
            }
        }
        en = System.nanoTime();
        System.out.println("\n4   time " + (en - st)/1000000.d + " msc");
    }
}

两次71.998012 msc

两次551.664166 msc

3次63.74851 msc

4次57.215167 msc

P.S。我不是一个java spec =)

答案 3 :(得分:0)

  

我知道,你为第二次运行分配了一个新数组,但是,你是否尝试改变“未优化”和“优化”运行的顺序? - fikto

我改变了它们的顺序并稍微优化了一下:

class A {
  public static void main(String[] args) {
    int n = 8009;
    double q1, q2;
    long st, en;

    // two
    int g[][] = new int[n][n];
    st = System.nanoTime();
    int odd = (n%2), l=n-odd;
    for(int i = 0; i < l; ++i) {
      int t0, t1;   
      int a0[] = g[t0 = i];
      int a1[] = g[t1 = ++i];
      for(int j = 0; j < n; ++j, ++t0, ++t1) {
        a0[j] = t0;
        a1[j] = t1;
      }
    }
    if(odd != 0)
    {
      int i = n-1;
      int a[] = g[i];
      for(int j = 0; j < n; ++j, ++i) {
        a[j] = i;
      }
    }
    en = System.nanoTime();
    System.out.println("Optimized time " + (q1=(en - st)/1000000.d) + " msc");

    // one
    int gg[][] = new int[n][n];
    st = System.nanoTime();
    for(int i = 0; i < n; i++) {
      for(int j = 0; j < n; j++) {
        gg[i][j] = i + j; 
      }
    }
    en = System.nanoTime();

    System.out.println("One time " + (q2=(en - st)/1000000.d) + " msc");

    System.out.println("1 - T1/T2 = " + (1 - q1/q2));

  }
}

结果是:

Optimized time 99.360293 msc
One time 162.23607 msc
1 - T1/T2 = 0.3875573231033026