Чи створює якийсь компілятор 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
Білл,


3
Насправді, згідно з цим блогом , JNI може бути досить швидким, якщо використовувати його "правильно".
ziggystar

2
Відповідне повідомлення в блозі з цього приводу можна знайти тут: psy-lob-saw.blogspot.com/2015/04/… із загальним повідомленням про те, що векторизація може траплятися і трапляється. Окрім векторизації конкретних випадків (Arrays.fill () / дорівнює (char []) / arrayCopy), JVM автоматично векторизує, використовуючи паралелізацію рівня Superword. Відповідний код наведено у superword.cpp, а стаття, на якій він базується, знаходиться тут: groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf
Ніцан Вакарт,

Відповіді:


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 (IcedTea6 1.10.6) (fedora-63.1.10.6.fc15-x86_64) OpenJDK 64-бітний сервер VM (збірка 20.0-b11, змішаний режим)
Семюель Одет

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 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
З тієї ж статті: "Висновок розбірника JITed свідчить про те, що він насправді не настільки ефективний з точки зору виклику найбільш оптимальних інструкцій SIMD та їх планування. Швидке полювання через вихідний код компілятора JVM JIT (Hotspot) свідчить про те, що це пов'язано з відсутність упакованих кодів інструкцій SIMD ". Регістри SSE використовуються в скалярному режимі.
Олександр Дубінський

1
@AleksandrDubinsky деякі випадки висвітлюються, деякі ні. Чи є у вас конкретна справа, яка вас цікавить?
Нітсан Вакарт,

2
Давайте перекинемо питання і запитаємо, чи JVM буде автоматично авторизувати будь-які арифметичні операції? Можете навести приклад? У мене є цикл, який нещодавно мені довелося витягнути і переписати, використовуючи intrinsics. Однак, замість надії на автовекторизацію, я хотів би бачити підтримку явної векторизації / власних характеристик (подібно до agner.org/optimize/vectorclass.pdf ). Ще краще було б написати хороший бэкенд для Java для Aparapi (хоча керівництво цього проекту ставить перед собою неправильні цілі). Ви працюєте над JVM?
Олександр Дубінський

1
@AleksandrDubinsky Я сподіваюся, що розширена відповідь допоможе, якщо не, можливо, електронний лист допоможе. Також зауважте, що "переписувати за допомогою внутрішніх даних" означає, що ви змінили код JVM, щоб додати нові внутрішні характеристики, це те, що ви маєте на увазі? Я здогадуюсь, ви мали на увазі заміну коду Java на дзвінки на власну реалізацію через JNI
Ніцан Вакарт,

1
Дякую. Тепер це має бути офіційною відповіддю. Думаю, вам слід видалити посилання на цей документ, оскільки він застарів і не демонструє векторизації.
Олександр Дубінський

26

У версіях HotSpot, що починаються з Java 7u40, компілятор серверів забезпечує підтримку автоматичної векторизації. Відповідно до JDK-6340864

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


У деяких випадках векторизація існує і в JDK6, хоча цільовий набір інструкцій SIMD не такий широкий.
Ніцан Вакарт

3
Підтримка векторизації компілятора в HotSpot останнім часом значно покращилася (червень 2017 р.) Завдяки внеску від Intel. З точки зору продуктивності поки що не випущений jdk9 (b163 та пізніші версії) в даний час перемагає jdk8 завдяки виправленням помилок, що дозволяють AVX2. Цикли повинні виконувати декілька обмежень для автоматичної векторизації для роботи, наприклад, використовувати: лічильник int, постійне збільшення лічильника, одна умова завершення з інваріантними змінними циклу, тіло циклу без викликів методу (?), Відсутність розгортання циклу вручну! Деталі доступні за адресою
Ведран,

Підтримка векторизованого плавленого множинного додавання (FMA) наразі не виглядає добре (станом на червень 2017 року): це або векторизація, або скалярний FMA (?). Однак Oracle, очевидно, щойно прийняв внесок Intel до HotSpot, який дозволяє векторизувати FMA за допомогою AVX-512. На радість любителям автоматичної векторизації та тим щасливчикам, що вони мають доступ до апаратного забезпечення AVX-512, це може (за деякої удачі) з’явитися в одній з наступних збірок jdk9 EA (понад b175).
Ведран

Посилання на підтримку попереднього твердження (RFR (M): 8181616: Векторизація FMA на x86): mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/…
Ведран,

2
Невеликий орієнтир, що демонструє прискорення в 4 рази цілих чисел за допомогою векторизації циклу з використанням інструкцій AVX2: prestodb.rocks/code/simd
Ведран,

6

Ось приємна стаття про експерименти з інструкціями Java та SIMD, написана моїм другом: http://prestodb.rocks/code/simd/

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


1
Було б корисно, якщо б ви узагальнили деякі ключові ідеї статті, на яку ви посилалися.
Олександр Дубінський

4

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

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


4

Погляньте на порівняння продуктивності між Java та JNI для оптимальної реалізації обчислювальних мікроядер . Вони показують, що компілятор серверів Java HotSpot VM підтримує автоматичну векторизацію за допомогою паралелізму рівня Super-word, який обмежений простими випадками паралелізму всередині циклу. Ця стаття також дасть вам деякі вказівки щодо того, чи є ваш обсяг даних достатньо великим, щоб виправдати проходження маршруту JNI.


3

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


1
Так, давно. Я більше сподівався почути, що це автоматично перекладається у векторизовані інструкції. Але очевидно, що не так важко зробити це вручну.
Шон Оуен

-4

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


3
В даний час жоден компілятор точки доступу Java не робить цього, але це не набагато складніше, ніж те, що вони роблять. Вони використовують інструкції SIMD для копіювання кількох значень масиву одночасно. Вам просто потрібно написати ще кілька збігів шаблонів та коду генерації коду, що досить просто після розгортання циклу. Я думаю, що люди в Sun просто полінувались, але, схоже, це зараз відбудеться в Oracle (о, Володимире! Це має дуже допомогти нашому коду!): Mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ ...
Крістофер Меннінг,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.