JVM의 JIT 컴파일러가 벡터화 된 부동 소수점 명령어를 사용하는 코드를 생성합니까?


95

내 Java 프로그램의 병목 현상이 실제로 많은 벡터 내적을 계산하기위한 타이트한 루프라고 가정 해 보겠습니다. 예, 프로필을 작성했습니다. 예, 병목 현상입니다. 예, 중요합니다. 예, 알고리즘이 그렇습니다. 예, 바이트 코드를 최적화하기 위해 Proguard를 실행했습니다.

작업은 본질적으로 내적입니다. 에서와 같이 두 개가 float[50]있고 쌍을 이루는 곱의 합을 계산해야합니다. SSE 또는 MMX와 ​​같이 이러한 종류의 작업을 대량으로 신속하게 수행하기 위해 프로세서 명령 세트가 존재한다는 것을 알고 있습니다.

예, JNI에서 일부 네이티브 코드를 작성하여 액세스 할 수 있습니다. JNI 호출은 상당히 비쌉니다.

JIT가 컴파일하거나 컴파일하지 않을 것을 보장 할 수 없다는 것을 알고 있습니다. 사람이나요 지금 이 지침을 사용하는 JIT 생성 코드 들어? 그렇다면 Java 코드에 대해 이러한 방식으로 컴파일 할 수 있도록 도와주는 것이 있습니까?

아마도 "아니오"일 것입니다. 물어볼 가치가 있습니다.


4
가장 쉽게 찾을 수있는 방법은 찾을 수있는 가장 최신 JIT를 가져와 생성 된 어셈블리를 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation. 벡터화 가능 메서드를 "핫"할 수있을만큼 충분히 실행하는 프로그램이 필요합니다.
Louis Wasserman

1
또는 출처를 살펴보십시오. download.java.net/openjdk/jdk7
Bill


3
실제로이 블로그 에 따르면 JNI는 "올바르게"사용하면 다소 빠를 수 있습니다.
ziggystar

2
이에 대한 관련 블로그 게시물은 여기에서 찾을 수 있습니다. psy-lob-saw.blogspot.com/2015/04/… 벡터화가 발생할 수 있다는 일반적인 메시지가 포함되어 있습니다. 특정 사례를 벡터화하는 것 외에도 (Arrays.fill () / equals (char []) / arrayCopy) JVM은 수퍼 워드 수준 병렬화를 사용하여 자동 벡터화합니다. 관련 코드는 superword.cpp에 있으며 그 기반이되는 논문은 여기에 있습니다 : groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf
Nitsan Wakart

답변:


44

따라서 기본적으로 코드가 더 빠르게 실행되기를 원합니다. JNI가 답입니다. 당신이 그것이 당신에게 효과가 없다고 말한 것을 알고 있지만 당신이 틀렸다는 것을 보여 드리겠습니다.

여기 있습니다 Dot.java:

import java.nio.FloatBuffer;
import org.bytedeco.javacpp.*;
import org.bytedeco.javacpp.annotation.*;

@Platform(include = "Dot.h", compiler = "fastfpu")
public class Dot {
    static { Loader.load(); }

    static float[] a = new float[50], b = new float[50];
    static float dot() {
        float sum = 0;
        for (int i = 0; i < 50; i++) {
            sum += a[i]*b[i];
        }
        return sum;
    }
    static native @MemberGetter FloatPointer ac();
    static native @MemberGetter FloatPointer bc();
    static native @NoException float dotc();

    public static void main(String[] args) {
        FloatBuffer ab = ac().capacity(50).asBuffer();
        FloatBuffer bb = bc().capacity(50).asBuffer();

        for (int i = 0; i < 10000000; i++) {
            a[i%50] = b[i%50] = dot();
            float sum = dotc();
            ab.put(i%50, sum);
            bb.put(i%50, sum);
        }
        long t1 = System.nanoTime();
        for (int i = 0; i < 10000000; i++) {
            a[i%50] = b[i%50] = dot();
        }
        long t2 = System.nanoTime();
        for (int i = 0; i < 10000000; i++) {
            float sum = dotc();
            ab.put(i%50, sum);
            bb.put(i%50, sum);
        }
        long t3 = System.nanoTime();
        System.out.println("dot(): " + (t2 - t1)/10000000 + " ns");
        System.out.println("dotc(): "  + (t3 - t2)/10000000 + " ns");
    }
}

Dot.h:

float ac[50], bc[50];

inline float dotc() {
    float sum = 0;
    for (int i = 0; i < 50; i++) {
        sum += ac[i]*bc[i];
    }
    return sum;
}

다음 명령을 사용하여 JavaCPP로 컴파일하고 실행할 수 있습니다 .

$ java -jar javacpp.jar Dot.java -exec

Intel (R) Core (TM) i7-7700HQ CPU @ 2.80GHz, Fedora 30, GCC 9.1.1 및 OpenJDK 8 또는 11을 사용하면 다음과 같은 출력이 나타납니다.

dot(): 39 ns
dotc(): 16 ns

또는 대략 2.4 배 더 빠릅니다. 어레이 대신 직접 NIO 버퍼를 사용해야하지만 HotSpot은 어레이만큼 빠르게 직접 NIO 버퍼에 액세스 할 수 있습니다 . 반면에 루프를 수동으로 풀면이 경우 성능이 크게 향상되지 않습니다.


3
OpenJDK 또는 Oracle HotSpot을 사용하셨습니까? 대중적인 믿음과는 달리, 그들은 동일하지 않습니다.
조나단 S. 피셔

@exabrial 이것이 바로이 시스템에서 "java -version"이 반환하는 것입니다. java 버전 "1.6.0_22"OpenJDK 런타임 환경 (IcedTea6 1.10.6) (fedora-63.1.10.6.fc15-x86_64) OpenJDK 64 비트 서버 VM (20.0-B11, 혼합 모드 구축)
사무엘 Audet

1
해당 루프에는 캐리 루프 종속성이있을 수 있습니다. 루프를 두 번 이상 풀면 속도가 더 빨라질 수 있습니다.

3
@Oliv GCC는 SSE를 사용하여 코드를 벡터화합니다. 예,하지만 이러한 작은 데이터의 경우 JNI 호출 오버 헤드가 안타깝게도 너무 큽니다.
Samuel Audet

2
JDK 13을 사용하는 A6-7310에서 dot () : 69ns / dotc () : 95ns를 얻습니다. 자바가 이긴다!
Stefan Reich

39

여기에서 다른 사람들이 표현한 회의론을 다루기 위해 본인 또는 다른 사람에게 증명하고 싶은 사람은 누구나 다음 방법을 사용할 것을 제안합니다.

  • JMH 프로젝트 만들기
  • 벡터화 가능한 수학의 작은 조각을 작성하십시오.
  • -XX : -UseSuperWord와 -XX ​​: + UseSuperWord (기본값) 사이에서 벤치 마크 뒤집기를 실행합니다.
  • 성능 차이가 관찰되지 않으면 코드가 벡터화되지 않은 것입니다.
  • 확인하려면 어셈블리를 인쇄하도록 벤치 마크를 실행하십시오. Linux에서는 perfasm 프로파일 러 ( '-prof perfasm')를보고 예상 한 지침이 생성되는지 확인할 수 있습니다.

예:

@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE) //makes looking at assembly easier
public void inc() {
    for (int i=0;i<a.length;i++)
        a[i]++;// a is an int[], I benchmarked with size 32K
}

플래그를 포함하거나 포함하지 않은 결과 (최신 Haswell 랩톱, Oracle JDK 8u60) : -XX : + UseSuperWord : 475.073 ± 44.579 ns / op (op 당 나노초) -XX : -UseSuperWord : 3376.364 ± 233.211 ns / op

핫 루프에 대한 어셈블리는 형식을 지정하고 여기에 고정해야하지만 여기에 스 니펫이 있습니다 (hsdis.so는 일부 AVX2 벡터 명령의 형식을 지정하지 못하므로 -XX : UseAVX = 1) : -XX : + UseSuperWord ( '-prof perfasm : intelSyntax = true'사용)

  9.15%   10.90%  │││ │↗    0x00007fc09d1ece60: vmovdqu xmm1,XMMWORD PTR [r10+r9*4+0x18]
 10.63%    9.78%  │││ ││    0x00007fc09d1ece67: vpaddd xmm1,xmm1,xmm0
 12.47%   12.67%  │││ ││    0x00007fc09d1ece6b: movsxd r11,r9d
  8.54%    7.82%  │││ ││    0x00007fc09d1ece6e: vmovdqu xmm2,XMMWORD PTR [r10+r11*4+0x28]
                  │││ ││                                                  ;*iaload
                  │││ ││                                                  ; - psy.lob.saw.VectorMath::inc@17 (line 45)
 10.68%   10.36%  │││ ││    0x00007fc09d1ece75: vmovdqu XMMWORD PTR [r10+r9*4+0x18],xmm1
 10.65%   10.44%  │││ ││    0x00007fc09d1ece7c: vpaddd xmm1,xmm2,xmm0
 10.11%   11.94%  │││ ││    0x00007fc09d1ece80: vmovdqu XMMWORD PTR [r10+r11*4+0x28],xmm1
                  │││ ││                                                  ;*iastore
                  │││ ││                                                  ; - psy.lob.saw.VectorMath::inc@20 (line 45)
 11.19%   12.65%  │││ ││    0x00007fc09d1ece87: add    r9d,0x8            ;*iinc
                  │││ ││                                                  ; - psy.lob.saw.VectorMath::inc@21 (line 44)
  8.38%    9.50%  │││ ││    0x00007fc09d1ece8b: cmp    r9d,ecx
                  │││ │╰    0x00007fc09d1ece8e: jl     0x00007fc09d1ece60  ;*if_icmpge

성을 습격하는 재미를 느껴보세요!


1
같은 문서에서 : "JIT 된 디스어셈블러 출력은 최적의 SIMD 명령어 호출 및 스케줄링 측면에서 실제로 그렇게 효율적이지 않음을 나타냅니다. JVM JIT 컴파일러 (핫스팟) 소스 코드를 통한 빠른 검색은 이것이 원인이라는 것을 암시합니다. 패킹 된 SIMD 명령어 코드가 존재하지 않습니다. " SSE 레지스터는 스칼라 모드에서 사용되고 있습니다.
Aleksandr Dubinsky

1
@AleksandrDubinsky 일부 사례는 다루고 일부는 그렇지 않습니다. 관심이있는 구체적인 사례가 있습니까?
Nitsan Wakart 2015 년

2
질문을 뒤집고 JVM이 산술 연산을 자동 벡터화할지 여부를 물어 보겠습니다. 예를 들어 줄 수 있습니까? 최근에 intrinsics를 사용하여 다시 작성해야하는 루프가 있습니다. 그러나 자동 벡터화에 대한 희망보다는 명시 적 벡터화 / 내재 함수 ( agner.org/optimize/vectorclass.pdf 와 유사)에 대한 지원을보고 싶습니다 . Aparapi를위한 좋은 자바 백엔드를 작성하는 것이 더 좋을 것입니다 (해당 프로젝트의 리더십이 잘못된 목표를 가지고 있음에도 불구하고). JVM에서 작업합니까?
Aleksandr Dubinsky 2015 년

1
@AleksandrDubinsky 이메일이 아닐지라도 확장 된 답변이 도움이되기를 바랍니다. 또한 "내장 함수를 사용하여 다시 작성"은 새 내장 함수를 추가하기 위해 JVM 코드를 변경했음을 의미합니다. 그게 무슨 뜻입니까? 난 당신이 JNI를 통해 네이티브 구현에 전화하여 자바 코드를 대체하는 의미 같은데요
Nitsan Wakart

1
감사합니다. 이것은 이제 공식적인 대답이되어야합니다. 논문에 대한 참조는 구식이고 벡터화를 보여주지 않기 때문에 제거해야한다고 생각합니다.
Aleksandr Dubinsky 2015.10.15

26

Java 7u40으로 시작하는 HotSpot 버전에서 서버 컴파일러는 자동 벡터화를 지원합니다. 에 따르면 JDK-6340864

그러나 이것은 "단순 루프"에 대해서만 사실 인 것 같습니다-적어도 현재로서는. 예를 들어 배열 누적은 아직 벡터화 할 수 없습니다. JDK-7192383


벡터화는 JDK6에서도 일부 경우에 존재하지만 대상 SIMD 명령어 세트는 그다지 넓지 않습니다.
Nitsan Wakart

3
HotSpot의 컴파일러 벡터화 지원은 인텔의 기여로 인해 최근 (2017 년 6 월)에 많이 개선되었습니다. 성능면에서 아직 릴리스되지 않은 jdk9 (b163 이상)는 AVX2를 활성화하는 버그 수정으로 인해 현재 jdk8보다 우위에 있습니다. 루프는 자동 벡터화가 작동하기 위해 몇 가지 제약 조건을 충족해야합니다. 예 : int 카운터, 상수 카운터 증가, 루프 불변 변수가있는 하나의 종료 조건, 메서드 호출이없는 루프 본문 (?), 수동 루프 전개 없음! 세부에서 사용할 수 있습니다 cr.openjdk.java.net/~vlivanov/talks/...
베드 란

벡터화 된 융합 다중 추가 (FMA) 지원은 현재 (2017 년 6 월 현재) 좋지 않습니다. 벡터화 또는 스칼라 FMA (?)입니다. 그러나 Oracle은 AVX-512를 사용하여 FMA 벡터화를 가능하게하는 HotSpot에 대한 Intel의 공헌을 방금 받아 들였습니다. 자동 벡터화 팬과 AVX-512 하드웨어에 액세스 할 수있는 운이 좋은 팬의 기쁨을 위해 다음 jdk9 EA 빌드 중 하나 (b175 이상)에 나타날 수 있습니다.
Vedran

이전 진술을 지원하는 링크 (RFR (M) : 8181616 : x86의 FMA 벡터화) : mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/…
Vedran

2
AVX2 명령어를 사용하여 루프 벡터화를 통해 정수에 4 배 가속을 보여주는 작은 벤치 마크 : prestodb.rocks/code/simd
베드 란

6

내 친구가 작성한 Java 및 SIMD 지침을 실험하는 방법에 대한 좋은 기사가 있습니다. http://prestodb.rocks/code/simd/

일반적인 결과는 JIT가 1.8에서 일부 SSE 작업을 사용하고 1.9에서 더 많이 사용할 것으로 예상 할 수 있다는 것입니다. 많은 것을 기 대해서는 안되며 조심해야합니다.


1
링크 한 기사의 주요 통찰력을 요약하면 도움이 될 것입니다.
Aleksandr Dubinsky

4

OpenCl 커널을 작성하여 컴퓨팅을 수행하고 java http://www.jocl.org/ 에서 실행할 수 있습니다 .

코드는 CPU 및 / 또는 GPU에서 실행할 수 있으며 OpenCL 언어는 벡터 유형도 지원하므로 SSE3 / 4 명령어 등을 명시 적으로 활용할 수 있어야합니다.



3

netlib-java에 대해 알기 전에이 질문을 썼다고 생각합니다. ;-) 기계 최적화 구현과 함께 필요한 기본 API를 정확히 제공하며 메모리 고정 덕분에 기본 경계에서 비용이 발생하지 않습니다.


1
예, 오래 전에 요 나는 이것이 자동으로 벡터화 된 명령으로 번역된다는 말을 듣고 싶었다. 그러나 분명히 수동으로 수행하는 것이 그렇게 어렵지 않습니다.
Sean Owen

-4

나는 어떤 VM이 이런 종류의 최적화를 위해 충분히 똑똑하다고 믿지 않는다. 공정하게 말하면 대부분의 최적화는 2의 거듭 제곱 일 때 곱하기 대신 이동하는 것과 같이 훨씬 더 간단합니다. 모노 프로젝트는 성능을 돕기 위해 네이티브 백업과 함께 자체 벡터 및 기타 방법을 도입했습니다.


3
현재 Java 핫스팟 컴파일러는이 작업을 수행하지 않지만 수행하는 작업보다 어렵지 않습니다. SIMD 명령어를 사용하여 한 번에 여러 배열 값을 복사합니다. 패턴 매칭과 코드 생성 코드를 좀 더 작성하면되는데, 이는 루프 언 롤링을 한 후 매우 간단합니다. Sun의 사람들이 방금 게으르다 고 생각하지만 이제 Oracle에서 일어날 것 같습니다 (예, Vladimir! 이것은 우리 코드에 많은 도움이 될 것입니다!) : mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ ...
크리스토퍼 매닝
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.