2

데이터 유형에 너무 많은 걱정을하지 않아도 (즉 중복 코드가 너무 많지 않음) 일부 이미지 분석 알고리즘을 구현하려면 자바의 프리미티브 배열에 대한 방문자 패턴을 설정하고 있습니다.자바 퍼포먼스 : 래퍼 클래스는 기본 유형보다 빠릅니까?

아래의 예에서

, 나는 방문자

  • visit 방법의 서명이 visit(int, int double)
  • 일반적인 유형입니다 원시적 형의 visit 방법의 서명의 두 가지 유형을 정의했다 visit(int, int Double)입니다.

Appart에서 두 방문자가 정확히 동일한 작업을 수행합니다. 내 생각은 권투/unboxing의 비용을 시도하고 측정하는 것이 었습니다.

그래서 나는 여기가 JIT 꽤 영리한 것을 알고 전체 프로그램

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 컴파일러가 작업을 할 수 있도록하기 위해) 예열 단계를 설명하는 코드를 업데이트 한 [편집]. 이 결과는 일관되게 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

작은 팁 :

  • 이 결과로 벤치 마크를 수행 할 Math.random()를 사용하지 마십시오는 비 결정적이다. new Random(xxx)과 같은 스턴스가 필요합니다.
  • 항상 작업 결과를 인쇄하십시오. 단일 실행에서 벤치 마크 유형을 혼합하는 것은 다른 호출 사이트 최적화로 이어질 수 있으므로 바람직하지 않습니다. (귀하의 경우는 아님)
  • double aux = System.nanoTime(); - longs이 모두 복식에 들어 가지 않는 것은 아닙니다.GC의이 '잘못'테스트시에 찰 수 -
  • 는 동안 컴파일 -XX:-PrintCompilation가비지 컬렉션-verbosegc -XX:+PrintGCDetails 인쇄 활성화 '시험 응시'
  • 인쇄에 벤치 마크를 수행하는 환경과 하드웨어의 사양을 게시 결과를 왜곡하기에 충분합니다.


편집 : 내가 한

생성 된 어셈블러를 확인하고 그들 중 누구도 진짜 이유 없다. Double.valueOf()에 대한 할당은 메소드가 모두 인라인되고 최적화 된 상태이므로 할당되지 않습니다. CPU 레지스터 만 사용합니다. 그러나 하드웨어 사양/JVM이 없으면 실제 대답은 없습니다.

나는 때문에 더 깊은 (분명히 EA Double.valueOf()에 필요) 분석 및 가능 constant folding 너비/높이의로, 일반 버전 ( Double가) 더 나은 루프 풀다를 (!)가있는 JVM (1.6.0.26)를 발견했다. WIDTH/HEIGHT를 약간 prime numbers으로 변경하면 결과가 달라집니다.


결론은입니다 : 당신은 JVM을 최적화하는 방법을 알고 생성 된 머신 코드를 확인하지 않는 한 microbenchmarks를 사용하지 마십시오.


면책 조항 : 나는 어떤 JVM 엔지니어

+0

팁을 주셔서 감사합니다.이 문제에 대해 생각하지 않았으므로 마지막 부분에 집중하겠습니다.하지만이 결과의 이유는 두 루프의 순서를 변경하면 문제가되지 않는다고 생각합니다. – Sebastien

+0

@Sebastien, 나는 대답을 얻은 것 같아 – bestsss

0

이 완전히 "야생 개 다 추측"하지만 내가 복사가 스택에 바이트와는 상관이 생각을 생각합니다. 기본 double을 전달하는 것은 스택에 8 바이트를 복사하는 것을 포함합니다. Double을 건네 주면 포인터 복사 만됩니다.

+0

이것은 사실 일 수 없다 - 그 방법은 하나의 호출 사이트, 즉 정적이다 - JVM은 확실히 인라인을해야한다. – bestsss

+0

사실이 아니라면 Byte보다 byte가 더 빠르지 만 Double보다 double이 더 느린 이유는 무엇입니까? –

+1

생성 된 어셈블리'(-server -XX : + UnlockDiagnosticVMOptions -XX : + PrintAssembly)'를 검사했습니다. 두 방법 모두 절대적으로 인라인되고'Double.valueOf()'는 생략됩니다 (즉, 전혀 존재하지 않습니다). Bytes.valueOf()는 btw로 할당되지 않으며 항상 캐시됩니다. – bestsss