2

为了实现一些图像分析算法,而不必担心数据类型过多(即没有太多重复的代码),我正在为Java中的原始数组设置访问者模式。Java性能困惑:包装类比原始类型更快?

在下面的例子中,我定义两种类型的访问者

  • 基本类型,其中visit方法的签名是visit(int, int double)
  • 一个通用类型,visit方法,其中所述签名的是visit(int, int Double)

从这个公寓,这两个游客做了完全相同的操作。我的想法是尝试衡量拳击/拆箱的成本。

因此,这里是完整的程序

public class VisitorsBenchmark { 
    public interface Array2DGenericVisitor<TYPE, RET> { 

     void begin(int width, int height); 

     RET end(); 

     void visit(int x, int y, TYPE value); 
    } 

    public interface Array2DPrimitiveVisitor<RET> { 

     void begin(final int width, final int height); 

     RET end(); 

     void visit(final int x, final int y, final double value); 
    } 

    public static <RET> 
     RET 
     accept(final int width, 
       final int height, 
       final double[] data, 
       final Array2DGenericVisitor<Double, RET> visitor) { 

     final int size = width * height; 
     visitor.begin(width, height); 
     for (int i = 0, x = 0, y = 0; i < size; i++) { 
      visitor.visit(x, y, data[i]); 
      x++; 
      if (x == width) { 
       x = 0; 
       y++; 
       if (y == height) { 
        y = 0; 
       } 
      } 
     } 
     return visitor.end(); 
    } 

    public static <RET> RET accept(final int width, 
            final int height, 
            final double[] data, 
            final Array2DPrimitiveVisitor<RET> visitor) { 

     final int size = width * height; 
     visitor.begin(width, height); 
     for (int i = 0, x = 0, y = 0; i < size; i++) { 
      visitor.visit(x, y, data[i]); 
      x++; 
      if (x == width) { 
       x = 0; 
       y++; 
       if (y == height) { 
        y = 0; 
       } 
      } 
     } 
     return visitor.end(); 
    } 

    private static final Array2DGenericVisitor<Double, double[]> generic; 

    private static final Array2DPrimitiveVisitor<double[]> primitive; 

    static { 
     generic = new Array2DGenericVisitor<Double, double[]>() { 
      private double[] sum; 

      @Override 
      public void begin(final int width, final int height) { 

       final int length = (int) Math.ceil(Math.hypot(WIDTH, HEIGHT)); 
       sum = new double[length]; 
      } 

      @Override 
      public void visit(final int x, final int y, final Double value) { 

       final int r = (int) Math.round(Math.sqrt(x * x + y * y)); 
       sum[r] += value; 
      } 

      @Override 
      public double[] end() { 

       return sum; 
      } 
     }; 

     primitive = new Array2DPrimitiveVisitor<double[]>() { 
      private double[] sum; 

      @Override 
      public void begin(final int width, final int height) { 

       final int length = (int) Math.ceil(Math.hypot(WIDTH, HEIGHT)); 
       sum = new double[length]; 
      } 

      @Override 
      public void visit(final int x, final int y, final double value) { 

       final int r = (int) Math.round(Math.sqrt(x * x + y * y)); 
       sum[r] += value; 
      } 

      @Override 
      public double[] end() { 

       return sum; 
      } 
     }; 
    } 

    private static final int WIDTH = 300; 

    private static final int HEIGHT = 300; 

    private static final int NUM_ITERATIONS_PREHEATING = 10000; 

    private static final int NUM_ITERATIONS_BENCHMARKING = 10000; 

    public static void main(String[] args) { 

     final double[] data = new double[WIDTH * HEIGHT]; 
     for (int i = 0; i < data.length; i++) { 
      data[i] = Math.random(); 
     } 

     /* 
     * Pre-heating. 
     */ 
     for (int i = 0; i < NUM_ITERATIONS_PREHEATING; i++) { 
      accept(WIDTH, HEIGHT, data, generic); 
     } 
     for (int i = 0; i < NUM_ITERATIONS_PREHEATING; i++) { 
      accept(WIDTH, HEIGHT, data, primitive); 
     } 

     /* 
     * Benchmarking proper. 
     */ 
     double[] sumPrimitive = null; 
     double[] sumGeneric = null; 

     double aux = System.nanoTime(); 
     for (int i = 0; i < NUM_ITERATIONS_BENCHMARKING; i++) { 
      sumGeneric = accept(WIDTH, HEIGHT, data, generic); 
     } 
     final double timeGeneric = System.nanoTime() - aux; 

     aux = System.nanoTime(); 
     for (int i = 0; i < NUM_ITERATIONS_BENCHMARKING; i++) { 
      sumPrimitive = accept(WIDTH, HEIGHT, data, primitive); 
     } 
     final double timePrimitive = System.nanoTime() - aux; 

     System.out.println("prim = " + timePrimitive); 
     System.out.println("generic = " + timeGeneric); 
     System.out.println("generic/primitive = " 
          + (timeGeneric/timePrimitive)); 
    } 
} 

我知道,JIT是相当聪明的,所以我并没有太惊讶,如果游客都变成了同样表现出色。 更令人惊讶的是,通用访问者似乎执行比原始略快,这是意想不到的。我知道基准测试有时可能很困难,所以我一定做错了。你能发现错误吗?

非常感谢您的帮助! 塞巴斯蒂安

[编辑]我已经更新的代码占一个预热阶段(为了让JIT编译器完成其工作)。这不会改变结果,它始终低于1(0.95 - 0.98)。

+2

传递一个Primitive double需要在堆栈上复制8个字节。传递Double只需要复制指针。 –

+2

你应该把测量的任务放在不同的方法中,并运行几次直到它们被编译(10,000/15,000应该没问题)。然后在循环中运行它们并进行测量。 [这篇文章是必读](http://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java)。 – assylias

+1

如果我反复运行测试,其差异在0.99和1.06之间,泛型稍慢。 –

回答

2

我知道基准测试有时可能很困难,所以我必须做错了什么。你能发现错误吗?

我认为问题在于您的基准测试不考虑JVM热身。把你的主要方法的身体,并把它放到另一种方法。然后让您的main方法在一个循环中重复调用该新方法。最后,检查结果,并放弃通过JIT编译和其他预热效果扭曲的前几个。

+0

我更新了代码片段 – Sebastien

2

小tips:

  • 不要使用Math.random()执行基准测试的结果是不确定的。你需要像new Random(xxx)那样的水货。
  • 总是打印操作的结果。在一次执行中混合基准类型是不好的做法,因为它可能会导致不同的呼叫站点优化(不过您的情况并非如此)
  • double aux = System.nanoTime(); - 并非所有longs都适合双打 - 正确。
  • 后你
  • 打印“凝视试验”而启用的打印编译-XX:-PrintCompilation垃圾收集-verbosegc -XX:+PrintGCDetails执行基准环境和硬件规格 - 气相色谱可以在“错误”的测试过程中踢只是足以扭曲结果。


编辑:

我做了检查生成的汇编和他们都不是真正的原因。没有为Double.valueOf()分配内存,因为方法内联并且优化了 - 它只使用CPU寄存器。但是没有硬件规格/ JVM,没有真正的答案。

我发现了一个JVM(1.6.0.26),其中通用版(Double)具有更好的循环展开(!),由于更深入的分析(显然需要EA的Double.valueOf()),并可能constant folding宽/高。将WIDTH/HEIGHT更改为prime numbers,结果应该不同。


底线是:除非你知道如何JVM优化和检查生成机器码不使用微基准。


免责声明:我不是JVM工程师

+0

谢谢你的提示,我会专注于最后一个,因为我没有想到这个问题,但是,我不认为这是结果的原因,因为改变两个循环的顺序的确如此 – Sebastien

+0

@Sebastien,我想我得到了你的答案 – bestsss

0

这是一个完全“野生称职的猜测”,但我认为这是与复制字节到堆栈中。传递一个双精度元素涉及在堆栈上复制8个字节。传递Double只需要复制指针。

+0

这不可能是真的 - 该方法是一个调用站点,即静态 - JVM是肯定内联的。 – bestsss

+0

如果不是这样,为什么字节比字节更快,但比双倍速度更慢? –

+1

检查生成的程序集'(-server -XX:+ UnlockDiagnosticVMOptions -XX:+ PrintAssembly)' - 这两种方法都是内联的,Double.valueOf()没有被使用(即根本不存在)。 Bytes.valueOf()永远不会按顺序分配,并且总是被缓存。 – bestsss