Có trình biên dịch JIT nào của JVM tạo mã sử dụng các lệnh dấu phẩy động được vector hóa không?


95

Giả sử nút thắt cổ chai của chương trình Java của tôi thực sự là một số vòng lặp chặt chẽ để tính toán một loạt các sản phẩm chấm vector. Vâng, tôi đã phân tích, vâng, đó là nút cổ chai, vâng, nó quan trọng, vâng, đó chỉ là cách thuật toán, vâng, tôi đã chạy Proguard để tối ưu hóa mã byte, v.v.

Về cơ bản, công việc là sản phẩm chấm. Như trong, tôi có hai float[50]và tôi cần tính tổng các tích từng cặp. Tôi biết các tập lệnh bộ xử lý tồn tại để thực hiện các loại hoạt động này một cách nhanh chóng và hàng loạt, như SSE hoặc MMX.

Có, tôi có thể truy cập chúng bằng cách viết một số mã gốc trong JNI. Cuộc gọi JNI hóa ra khá đắt.

Tôi biết bạn không thể đảm bảo rằng JIT sẽ biên dịch hay không biên dịch. Có ai đã từng nghe nói về mã tạo JIT sử dụng các hướng dẫn này chưa? và nếu vậy, có điều gì về mã Java giúp làm cho nó có thể biên dịch theo cách này không?

Có lẽ là "không"; đáng hỏi.


4
Cách dễ nhất để tìm ra có lẽ là lấy JIT hiện đại nhất mà bạn có thể tìm thấy và yêu cầu nó xuất ra bản lắp ráp đã tạo -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation. Bạn sẽ cần một chương trình chạy phương thức vectorizable đủ số lần để làm cho nó "nóng".
Louis Wasserman

1
Hoặc có một cái nhìn vào nguồn. download.java.net/openjdk/jdk7
Hóa đơn


3
Trên thực tế, theo blog này , JNI có thể khá nhanh nếu được sử dụng "đúng cách".
ziggystar 14/12/12

2
Bạn có thể tìm thấy một bài đăng blog liên quan về vấn đề này tại đây: psy-lob-saw.blogspot.com/2015/04/… với thông điệp chung rằng vectơ hóa có thể xảy ra và sẽ xảy ra. Ngoài vectơ hóa các trường hợp cụ thể (Arrays.fill () / equals (char []) / arrayCopy), JVM còn tự động hóa vectơ bằng cách sử dụng Song song cấp độ siêu từ. Mã liên quan có trong superword.cpp và bài báo dựa trên nó ở đây: groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf
Nitsan Wakart,

Câu trả lời:


44

Vì vậy, về cơ bản, bạn muốn mã của mình chạy nhanh hơn. JNI là câu trả lời. Tôi biết bạn đã nói điều đó không hiệu quả với bạn, nhưng hãy để tôi cho bạn thấy rằng bạn đã sai.

Đây là 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;
}

Chúng tôi có thể biên dịch và chạy nó với JavaCPP bằng lệnh này:

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

Với CPU Intel (R) Core (TM) i7-7700HQ @ 2,80GHz, Fedora 30, GCC 9.1.1 và OpenJDK 8 hoặc 11, tôi nhận được loại đầu ra này:

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

Hoặc nhanh hơn khoảng 2,4 lần. Chúng ta cần sử dụng bộ đệm NIO trực tiếp thay vì mảng, nhưng HotSpot có thể truy cập bộ đệm NIO trực tiếp nhanh như mảng . Mặt khác, việc mở vòng lặp theo cách thủ công không mang lại hiệu suất tăng có thể đo lường được, trong trường hợp này.


3
Bạn đã sử dụng OpenJDK hoặc Oracle HotSpot? Trái ngược với niềm tin phổ biến, chúng không giống nhau.
Jonathan S. Fisher

@exabrial Đây là nội dung "java -version" trả về trên máy này ngay bây giờ: phiên bản java "1.6.0_22" OpenJDK Runtime Environment (IcedTea6 1.10.6) (fedora-63.1.10.6.fc15-x86_64) OpenJDK 64-Bit Server VM (bản dựng 20.0-b11, chế độ hỗn hợp)
Samuel Audet

1
Vòng lặp đó có thể có một phụ thuộc vòng lặp mang. Bạn có thể tăng tốc hơn nữa bằng cách mở vòng lặp hai lần trở lên.

3
@Oliv GCC vectơ mã bằng SSE, vâng, nhưng đối với dữ liệu nhỏ như vậy, chi phí cuộc gọi JNI rất tiếc là quá lớn.
Samuel Audet

2
Trên A6-7310 của tôi với JDK 13, tôi nhận được: dot (): 69 ns / dotc (): 95 ns. Java chiến thắng!
Stefan Reich

39

Để giải quyết một số hoài nghi do những người khác bày tỏ ở đây, tôi đề nghị bất kỳ ai muốn chứng minh với bản thân hoặc người khác hãy sử dụng phương pháp sau:

  • Tạo một dự án JMH
  • Viết một đoạn mã nhỏ của phép toán vectơ.
  • Chạy chuyển đổi điểm chuẩn của họ giữa -XX: -UseSuperWord và -XX: + UseSuperWord (mặc định)
  • Nếu không có sự khác biệt về hiệu suất được quan sát thấy, mã của bạn có thể không được vectơ hóa
  • Để đảm bảo, hãy chạy điểm chuẩn của bạn để nó in ra bản lắp ráp. Trên linux, bạn có thể tận hưởng trình biên dịch perfasm ('- prof perfasm') xem và xem liệu các hướng dẫn bạn mong đợi có được tạo hay không.

Thí dụ:

@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
}

Kết quả có và không có cờ (trên máy tính xách tay Haswell gần đây, Oracle JDK 8u60): -XX: + UseSuperWord: 475.073 ± 44.579 ns / op (nano giây mỗi op) -XX: -UseSuperWord: 3376.364 ± 233.211 ns / op

Phần lắp ráp cho vòng lặp nóng hơi nhiều để định dạng và dính vào đây nhưng đây là một đoạn mã (hsdis.so không thể định dạng một số hướng dẫn vectơ AVX2 nên tôi đã chạy với -XX: UseAVX = 1): -XX: + UseSuperWord (với '-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

Hãy vui vẻ khi xông vào lâu đài!


1
Từ cùng một bài báo: "đầu ra của trình tháo gỡ JITed cho thấy rằng nó không thực sự hiệu quả về mặt gọi các lệnh SIMD tối ưu nhất và lập lịch trình của chúng. Tìm kiếm nhanh thông qua mã nguồn trình biên dịch JVM JIT (Hotspot) cho thấy rằng điều này là do sự không tồn tại của mã hướng dẫn SIMD được đóng gói. " Các thanh ghi SSE đang được sử dụng trong chế độ vô hướng.
Aleksandr Dubinsky

1
@AleksandrDubinsky một số trường hợp được bảo hiểm, một số trường hợp thì không. Bạn có một trường hợp cụ thể mà bạn quan tâm?
Nitsan Wakart vào

2
Hãy lật lại câu hỏi và đặt câu hỏi liệu JVM có tự động hóa bất kỳ phép toán số học nào không? bạn có thể cung cấp một ví dụ? Tôi có một vòng lặp mà tôi đã phải kéo ra và viết lại bằng cách sử dụng bản chất gần đây. Tuy nhiên, thay vì hy vọng vào quá trình tự động hóa, tôi muốn nhận được sự hỗ trợ cho phương pháp vectơ hóa / bản chất rõ ràng (tương tự như agner.org/optimize/vectorclass.pdf ). Thậm chí tốt hơn là viết một chương trình phụ trợ Java tốt cho Aparapi (mặc dù ban lãnh đạo của dự án đó có một số mục tiêu sai lầm). Bạn có làm việc trên JVM không?
Aleksandr Dubinsky

1
@AleksandrDubinsky Tôi hy vọng câu trả lời mở rộng sẽ giúp ích, nếu không có lẽ là một email. Cũng lưu ý rằng "viết lại bằng cách sử dụng bản chất" ngụ ý bạn đã thay đổi mã JVM để thêm bản chất mới, đó có phải là ý bạn? Tôi đoán bạn có nghĩa là thay thế mã Java của bạn với các cuộc gọi vào một việc thực hiện bản địa thông qua JNI
Nitsan Wakart

1
Cảm ơn bạn. Đây là câu trả lời chính thức. Tôi nghĩ bạn nên xóa tham chiếu đến bài báo, vì nó đã lỗi thời và không thể hiện được sự vector hóa.
Aleksandr Dubinsky

26

Trong các phiên bản HotSpot bắt đầu bằng Java 7u40, trình biên dịch máy chủ cung cấp hỗ trợ tự động vectorisation. Theo JDK-6340864

Tuy nhiên, điều này dường như chỉ đúng với "vòng lặp đơn giản" - ít nhất là ở thời điểm hiện tại. Ví dụ: tích lũy một mảng chưa thể được biểu diễn bằng vectơ JDK-7192383


Vectơ hóa cũng có trong JDK6 đối với một số trường hợp, mặc dù tập lệnh SIMD được nhắm mục tiêu không rộng bằng.
Nitsan Wakart

3
Gần đây, hỗ trợ vector hóa trình biên dịch trong HotSpot đã được cải thiện rất nhiều (tháng 6 năm 2017) do sự đóng góp của Intel. Hiệu suất khôn ngoan, jdk9 chưa được phát hành (b163 trở lên) hiện đang thắng jdk8 do các bản sửa lỗi cho phép AVX2. Các vòng lặp phải đáp ứng một số ràng buộc để tự động vectơ hoạt động, ví dụ: sử dụng: bộ đếm int, tăng số bộ đếm không đổi, một điều kiện kết thúc với các biến bất biến của vòng lặp, thân vòng lặp không có lệnh gọi phương thức (?), Không có vòng lặp thủ công mở ra! Thông tin chi tiết có tại: cr.openjdk.java.net/~vlivanov/talks/…
Vedran

Hỗ trợ được kết hợp nhiều-bổ sung (FMA) được vector hóa hiện không tốt (tính đến tháng 6 năm 2017): đó là vector hóa hoặc FMA vô hướng (?). Tuy nhiên, Oracle dường như vừa chấp nhận sự đóng góp của Intel cho HotSpot cho phép vector hóa FMA bằng AVX-512. Trước sự hài lòng của những người hâm mộ tự động hóa vectơ và những người may mắn có quyền truy cập vào phần cứng AVX-512, điều này có thể (với một số may mắn) xuất hiện trong một trong những bản dựng jdk9 EA tiếp theo (ngoài b175).
Vedran

Liên kết hỗ trợ các tuyên bố trước đó (RFR (M): 8.181.616: FMA bản vẽ Gia trên x86): mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/...
Vedran

2
Một benchmark nhỏ thể hiện khả năng tăng tốc bởi một yếu tố của 4 trên nguyên thông qua vòng lặp vector sử dụng hướng dẫn AVX2: prestodb.rocks/code/simd
Vedran

6

Đây là bài viết hay về thử nghiệm với Java và hướng dẫn SIMD do bạn tôi viết: http://prestodb.rocks/code/simd/

Kết quả chung của nó là bạn có thể mong đợi JIT sử dụng một số hoạt động SSE trong 1.8 (và một số hoạt động khác trong 1.9). Mặc dù vậy bạn không nên mong đợi nhiều và bạn cần phải cẩn thận.


1
Sẽ hữu ích nếu bạn tóm tắt một số thông tin chi tiết chính về bài viết mà bạn liên kết đến.
Aleksandr Dubinsky

4

Bạn có thể viết hạt nhân OpenCl để thực hiện tính toán và chạy nó từ java http://www.jocl.org/ .

Mã có thể được chạy trên CPU và / hoặc GPU và ngôn ngữ OpenCL cũng hỗ trợ các loại vectơ, vì vậy bạn sẽ có thể tận dụng lợi thế rõ ràng của các lệnh SSE3 / 4.


4

Hãy xem so sánh Hiệu suất giữa Java và JNI để triển khai tối ưu các vi nhân tính toán . Họ cho thấy rằng trình biên dịch máy chủ Java HotSpot VM hỗ trợ tự động vectơ hóa bằng cách sử dụng Song song cấp siêu từ, được giới hạn trong các trường hợp đơn giản của song song bên trong vòng lặp. Bài viết này cũng sẽ cung cấp cho bạn một số hướng dẫn liệu kích thước dữ liệu của bạn có đủ lớn để phù hợp với việc đi theo tuyến JNI hay không.


3

Tôi đoán bạn đã viết câu hỏi này trước khi bạn tìm hiểu về netlib-java ;-) nó cung cấp chính xác API gốc mà bạn yêu cầu, với các triển khai được tối ưu hóa cho máy và không có bất kỳ chi phí nào ở ranh giới gốc do ghim bộ nhớ.


1
Ừ, lâu rồi. Tôi đã hy vọng nhiều hơn khi biết rằng điều này được dịch tự động thành các hướng dẫn được vector hóa. Nhưng rõ ràng là không khó để thực hiện nó theo cách thủ công.
Sean Owen

-4

Tôi không tin nhất nếu có bất kỳ máy ảo nào đủ thông minh để tối ưu hóa loại này. Công bằng mà nói, hầu hết các tối ưu đều đơn giản hơn nhiều, chẳng hạn như chuyển số thay vì nhân khi lũy thừa của hai. Dự án mono đã giới thiệu vectơ của riêng họ và các phương pháp khác với sự hỗ trợ gốc để giúp hiệu suất.


3
Hiện tại, không có trình biên dịch điểm phát sóng Java nào làm được điều này, nhưng nó không khó hơn nhiều so với những thứ mà họ làm. Họ sử dụng hướng dẫn SIMD để sao chép nhiều giá trị mảng cùng một lúc. Bạn chỉ cần viết thêm một số đối sánh mẫu và mã tạo mã, điều này khá đơn giản sau khi thực hiện một số thao tác mở vòng lặp. Tôi nghĩ mọi người ở Sun chỉ lười biếng thôi, nhưng có vẻ như điều đó sẽ xảy ra tại Oracle (vâng Vladimir! Điều này sẽ giúp ích rất nhiều cho mã của chúng tôi!): Mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ …
Christopher Manning
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.