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

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

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

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

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

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

9 ответов

Решение

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

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

  • Создать проект JMH
  • Напишите небольшой фрагмент векторизованной математики.
  • Запустите переключение их тестов между -XX:-UseSuperWord и -XX:+UseSuperWord(по умолчанию)
  • Если никакой разницы в производительности не наблюдается, ваш код, вероятно, не был векторизован
  • Чтобы убедиться в этом, запустите эталонный тест, чтобы он распечатал сборку. В Linux вы можете посмотреть на профилировщик перфазма ('-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 нс / операция (наносекунды за операцию) -XX:-UseSuperWord: 3376,364 ± 233,211 нс / операции

Сборка для горячего цикла немного сложна для форматирования и вставления, но вот фрагмент (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

Весело штурмуйте замок!

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

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

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

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

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

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

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

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

Java 16 представила Vector API (JEP 417, JEP 414, JEP 338). В настоящее время он находится в стадии «инкубации» (т. е. бета-версии), хотя любой может его использовать. Вероятно, он станет общедоступным в Java 19 или 20.

Это немного многословно, но должно быть надежным и портативным.

Следующий код можно переписать:

      void scalarComputation(float[] a, float[] b, float[] c) {
   assert a.length == b.length && b.length == c.length;
   for (int i = 0; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
   }
}

Использование векторного API:

      static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

void vectorComputation(float[] a, float[] b, float[] c) {
    assert a.length == b.length && b.length == c.length;
    int i = 0;
    int upperBound = SPECIES.loopBound(a.length);
    for (; i < upperBound; i += SPECIES.length()) {
        // FloatVector va, vb, vc;
        var va = FloatVector.fromArray(SPECIES, a, i);
        var vb = FloatVector.fromArray(SPECIES, b, i);
        var vc = va.mul(va)
                   .add(vb.mul(vb))
                   .neg();
        vc.intoArray(c, i);
    }
    for (; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
    }
}

Более новые сборки (например, Java 18) пытаются избавиться от этого последнего цикла for с помощью предикатных инструкций, но поддержка этого все еще предположительно несовершенна.

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

Другие вопросы по тегам