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

95

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

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

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

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

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

Шон Оуэн
источник
4
Самый простой способ узнать это, вероятно, получить самый современный JIT, который вы можете найти, и заставить его выводить сгенерированную сборку -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation. Вам понадобится программа, которая запускает векторизуемый метод достаточное количество раз, чтобы сделать его «горячим».
Луи Вассерман
1
Или взгляните на источник. download.java.net/openjdk/jdk7
Bill
1
«Скоро» в ближайшем к вам jdk
Джонатан С. Фишер
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 @ 2,80 ГГц, Fedora 30, GCC 9.1.1 и OpenJDK 8 или 11, я получаю такой вывод:

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

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

Сэмюэл Одет
источник
3
Вы использовали OpenJDK или Oracle HotSpot? Вопреки распространенному мнению, это не одно и то же.
Джонатан С. Фишер,
@exabrial Это то, что "java -version" возвращает на этом компьютере прямо сейчас: 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 (сборка 20.0-b11, смешанный режим)
Samuel Audet
1
Этот цикл, вероятно, имеет зависимость переносимого цикла. Вы можете получить дополнительное ускорение, развернув петлю два или более раз.
3
@Oliv GCC векторизует код с помощью SSE, да, но для таких небольших данных накладные расходы на вызовы JNI, к сожалению, слишком велики.
Samuel Audet
2
На моем A6-7310 с JDK 13 я получаю: dot (): 69 нс / dotc (): 95 нс. Java побеждает!
Стефан Райх
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 нс / операцию (наносекунды на операцию) -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

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

Ницан Вакарт
источник
1
Из той же статьи: «Вывод JIT-дизассемблера предполагает, что он на самом деле не так эффективен с точки зрения вызова наиболее оптимальных инструкций SIMD и их планирования. Быстрый поиск в исходном коде JIT-компилятора JVM (Hotspot) предполагает, что это связано с отсутствие упакованных кодов инструкций SIMD ". Регистры SSE используются в скалярном режиме.
Александр Дубинский
1
@AleksandrDubinsky некоторые случаи покрыты, некоторые нет. У вас есть конкретный случай, который вас интересует?
Nitsan Wakart
2
Давайте перебросим вопрос и спросим, ​​будет ли JVM автовекторизовать какие-либо арифметические операции? Вы можете привести пример? У меня есть цикл, который мне недавно пришлось вытащить и переписать с использованием встроенных функций. Однако вместо того, чтобы надеяться на автовекторизацию, я хотел бы видеть поддержку явной векторизации / встроенных функций (аналогично agner.org/optimize/vectorclass.pdf ). Еще лучше было бы написать хороший Java-бэкэнд для Aparapi (хотя у руководства этого проекта есть несколько неправильных целей). Вы работаете на JVM?
Александр Дубинский
1
@AleksandrDubinsky Я надеюсь, что развернутый ответ поможет, если бы не электронное письмо. Также обратите внимание, что «переписать с использованием встроенных функций» подразумевает, что вы изменили код JVM, добавив новые встроенные функции, вы это имеете в виду? Я предполагаю, что вы имели в виду заменить свой Java-код вызовами на нативную реализацию через JNI
Нитсан Вакарт
1
Спасибо. Теперь это должен быть официальный ответ. Я думаю, вам стоит убрать ссылку на статью, так как она устарела и не демонстрирует векторизацию.
Александр Дубинский
26

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

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

Ведран
источник
Векторизация присутствует и в JDK6 для некоторых случаев, хотя целевой набор инструкций SIMD не такой широкий.
Nitsan Wakart
3
Поддержка векторизации компилятора в HotSpot была значительно улучшена в последнее время (июнь 2017 г.) благодаря вкладу Intel. С точки зрения производительности еще не выпущенный jdk9 (b163 и новее) в настоящее время побеждает jdk8 из-за исправлений ошибок, включающих AVX2. Циклы должны удовлетворять нескольким ограничениям для работы автоматической векторизации, например, использовать: int counter, постоянное приращение счетчика, одно условие завершения с переменными, инвариантными для цикла, тело цикла без вызовов методов (?), Без ручного развертывания цикла! Подробности доступны в: cr.openjdk.java.net/~vlivanov/talks/…
Vedran
В настоящее время (по состоянию на июнь 2017 г.) поддержка векторизованного слитного множественного добавления (FMA) выглядит не очень хорошо: это либо векторизация, либо скалярная FMA (?). Однако Oracle, по-видимому, только что приняла участие Intel в HotSpot, которое обеспечивает векторизацию FMA с использованием AVX-512. К радости поклонников автоматической векторизации и тех, кому посчастливилось иметь доступ к оборудованию AVX-512, это может (при некоторой удаче) появиться в одной из следующих сборок jdk9 EA (помимо b175).
Vedran
Ссылка в поддержку предыдущего утверждения (RFR (M): 8181616: Векторизация FMA на x86): mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/…
Vedran
2
Небольшой тест, демонстрирующий ускорение в 4 раза на целых числах за счет векторизации цикла с использованием инструкций AVX2: prestodb.rocks/code/simd
Vedran
6

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

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

кокосинг
источник
1
Было бы полезно, если бы вы обобщили некоторые ключевые идеи статьи, на которую вы ссылаетесь.
Александр Дубинский
4

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

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

Микаэль Лепистё
источник
4

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

Пол Юрчак
источник
3

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

фоммил
источник
1
Да, давным-давно. Я больше надеялся услышать, что это автоматически переводится в векторизованные инструкции. Но очевидно, что это не так уж и сложно сделать вручную.
Шон Оуэн,
-4

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

mP.
источник
3
В настоящее время ни один компилятор точки доступа Java не делает этого, но это не намного сложнее, чем то, что они делают. Они действительно используют инструкции SIMD для одновременного копирования нескольких значений массива. Вам просто нужно написать еще немного кода сопоставления с образцом и генерации кода, что довольно просто после развертывания некоторого цикла. Я думаю, что люди в Sun просто ленились, но похоже, что теперь это произойдет и в Oracle (ура, Владимир! Это должно сильно помочь нашему коду!): Mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ …
Кристофер Мэннинг