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