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