Разве компиляторы JVM JIT генерируют код, который использует векторизованные инструкции с плавающей запятой?

Скажем, узким местом моей Java-программы действительно являются некоторые жесткие петли, чтобы вычислить кучу векторных точечных продуктов. Да, я профилировал, да, это узкое место, да, это важно, да, что именно так алгоритм, да, я запустил Proguard для оптимизации байтового кода и т.д.

Работа - это, по сути, точечные произведения. Как и в, у меня есть два float[50], и мне нужно вычислить сумму попарных продуктов. Я знаю, что существуют наборы инструкций процессоров для выполнения таких операций быстро и навалом, например SSE или MMX.

Да, я могу, возможно, получить к ним доступ, написав собственный код в JNI. Вызов JNI оказывается довольно дорогим.

Я знаю, что вы не можете гарантировать, что JIT будет компилировать или не компилировать. Кто-нибудь слышал о генерации кода JIT, который использует эти инструкции? и если да, то что-нибудь о коде Java, который помогает сделать его компилируемым таким образом?

Вероятно, "нет"; стоит спросить.

Ответ 1

Итак, в основном, вы хотите, чтобы ваш код работал быстрее. 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 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 с помощью командной строки:

$ javac -cp javacpp.jar Dot.java
$ java -jar javacpp.jar Dot
$ java -cp javacpp.jar:. Dot

С процессором Intel Core i7-3632QM с частотой 2.20 ГГц, Fedora 20, GCC 4.8.3 и OpenJDK 7 или 8, я получаю такой вывод:

dot(): 37 ns
dotc(): 23 ns

Или примерно в 1,6 раза быстрее. Нам нужно использовать прямые NIO-буферы вместо массивов, но HotSpot может обращаться к прямым буферам NIO так же быстро, как массивы. С другой стороны, ручное разворачивание цикла в этом случае не обеспечивает измеримого повышения производительности.

Ответ 2

Чтобы устранить некоторые из скептицизма, выраженные другими, я предлагаю всем, кто хочет доказать себе или другим, использовать следующий метод:

  • Создайте проект 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::[email protected] (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::[email protected] (line 45)
 11.19%   12.65%  │││ ││    0x00007fc09d1ece87: add    r9d,0x8            ;*iinc
                  │││ ││                                                  ; - psy.lob.saw.VectorMath::[email protected] (line 44)
  8.38%    9.50%  │││ ││    0x00007fc09d1ece8b: cmp    r9d,ecx
                  │││ │╰    0x00007fc09d1ece8e: jl     0x00007fc09d1ece60  ;*if_icmpge

Удачи в штурме замка!

Ответ 3

В версиях HotSpot, начинающихся с Java 7u40, компилятор сервера обеспечивает поддержку автоматической векторизации. Согласно JDK-6340864

Однако это, похоже, справедливо только для "простых циклов" - по крайней мере, на данный момент. Например, накопление массива еще не может быть векторизованным JDK-7192383

Ответ 4

Вы можете написать ядро ​​OpenCl для выполнения вычислений и запустить его из java http://www.jocl.org/.

Код может быть запущен на процессоре и/или графическом процессоре, а язык OpenCL поддерживает также типы векторов, поэтому вы должны иметь возможность явно использовать преимущества, например, Инструкции SSE3/4.

Ответ 5

Вот хорошая статья об экспериментах с инструкциями Java и SIMD, написанными моим другом: http://prestodb.rocks/code/simd/

Его общий результат состоит в том, что вы можете ожидать, что JIT будет использовать некоторые SSE-операции в 1,8 (и еще несколько в 1.9). Хотя вы не должны много ожидать, и вам нужно быть осторожным.

Ответ 6

Я предполагаю, что вы написали этот вопрос, прежде чем узнали о netlib-java;-) он предоставляет именно тот собственный API, который вам нужен, с оптимизированными машиной реализациями и не имеет каких-либо затрат на родной границе из-за памяти пиннинга.

Ответ 7

Посмотрите Сравнение производительности между Java и JNI для оптимальной реализации вычислительных микроядер. Они показывают, что компилятор сервера Java HotSpot VM поддерживает автоматическую векторизацию с использованием уровня суперслоя Parallelism, который ограничен просто случаем внутри цикла parallelism. В этой статье вы также найдете несколько рекомендаций, насколько ваш размер данных достаточно велик, чтобы оправдать переход на маршрут JNI.

Ответ 8

Я не верю больше всего, если любые виртуальные машины когда-либо достаточно умны для такого рода оптимизаций. Справедливости ради следует отметить, что большинство оптимизаций гораздо проще, например, вместо умножения, когда сила двух. Монопроект представил свои собственные векторные и другие методы с помощью собственных резервных копий, чтобы помочь производительности.