java - parte - ¿Los compiladores JIT de cualquier JVM generan código que usa instrucciones vectorizadas de punto flotante?
jit software (8)
Eche un vistazo a la comparación de rendimiento entre Java y JNI para la implementación óptima de micronúcleos computacionales . Muestran que el compilador del servidor Java HotSpot VM admite la auto-vectorización utilizando el Paralelismo de nivel de super palabras, que se limita a casos simples dentro del paralelismo de bucle. Este artículo también le dará alguna orientación sobre si su tamaño de datos es lo suficientemente grande como para justificar la ruta JNI.
Digamos que el cuello de botella de mi programa Java realmente es un poco difícil para calcular un montón de productos vector dot. Sí, tengo un perfil, sí, es el cuello de botella, sí, es significativo, sí, así es como funciona el algoritmo, sí, he ejecutado Proguard para optimizar el código de bytes, etc.
El trabajo es, esencialmente, productos punto. Como en, tengo dos float[50]
y necesito calcular la suma de los productos pairwise. Sé que existen conjuntos de instrucciones de procesador para realizar este tipo de operaciones de forma rápida y masiva, como SSE o MMX.
Sí, probablemente pueda acceder a ellos escribiendo algún código nativo en JNI. La llamada JNI resulta ser bastante costosa.
Sé que no puedes garantizar lo que un JIT compilará o no compilará. ¿Alguien ha oído hablar alguna vez de un código generador de JIT que utiliza estas instrucciones? y si es así, ¿hay algo sobre el código de Java que ayude a hacerlo compilable de esta manera?
Probablemente un "no"; vale la pena preguntar
En las versiones de HotSpot que comienzan con Java 7u40, el compilador del servidor proporciona soporte para la auto-vectorización. De acuerdo con JDK-6340864
Sin embargo, esto parece ser cierto solo para "bucles simples", al menos por el momento. Por ejemplo, la acumulación de una matriz no se puede vectorizar todavía JDK-7192383
Entonces, básicamente, quieres que tu código se ejecute más rápido. JNI es la respuesta. Sé que dijiste que no funcionó para ti, pero déjame mostrarte que estás equivocado.
Aquí está 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");
}
}
y 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;
}
Podemos compilar y ejecutar eso con JavaCPP usando comandos de línea estos:
$ javac -cp javacpp.jar Dot.java
$ java -jar javacpp.jar Dot
$ java -cp javacpp.jar:. Dot
Con una CPU Intel Core i7-3632QM a 2.20 GHz, Fedora 20, GCC 4.8.3 y OpenJDK 7 u 8, obtengo este tipo de resultados:
dot(): 37 ns
dotc(): 23 ns
O aproximadamente 1.6 veces más rápido. Necesitamos utilizar búferes directos de NIO en lugar de matrices, pero HotSpot puede acceder a búferes directos de NIO tan rápido como las matrices . Por otro lado, desenrollar manualmente el bucle no proporciona un aumento mensurable en el rendimiento, en este caso.
Este es un buen artículo sobre cómo experimentar con las instrucciones de Java y SIMD escritas por mi amigo: prestodb.rocks/code/simd
Su resultado general es que puede esperar que JIT use algunas operaciones SSE en 1.8 (y algunas más en 1.9). Aunque no deberías esperar mucho y debes tener cuidado.
No creo que la mayoría de las máquinas virtuales sean lo suficientemente inteligentes para este tipo de optimizaciones. Para ser justos, la mayoría de las optimizaciones son mucho más simples, como el cambio en lugar de la multiplicación cuando el poder es de dos. El proyecto mono introdujo su propio vector y otros métodos con respaldos nativos para ayudar al rendimiento.
Para abordar algunos de los escepticismos expresados por otros aquí, sugiero que cualquiera que quiera probarse a sí mismo u otros utilice el siguiente método:
- Crea un proyecto JMH
- Escribe un pequeño fragmento de matemáticas vectorizable.
- Ejecute su cambio de referencia entre -XX: -UseSuperWord y -XX: + UseSuperWord (predeterminado)
- Si no se observa ninguna diferencia en el rendimiento, es probable que tu código no se haya vectorizado
- Para asegurarse, ejecute su punto de referencia de manera que imprima el conjunto. En Linux, puedes disfrutar del perfilador perfumista (''- prof perfasm'') echa un vistazo y mira si las instrucciones que esperas se generan.
Ejemplo:
@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
}
El resultado con y sin la bandera (en la computadora portátil Haswell reciente, Oracle JDK 8u60): -XX: + UseSuperWord: 475.073 ± 44.579 ns / op (nanosegundos por opción) -XX: -UseSuperWord: 3376.364 ± 233.211 ns / op
El ensamblaje del bucle caliente tiene mucho que formatear y pegar aquí, pero aquí hay un fragmento (hsdis.so no está formateando algunas de las instrucciones del vector AVX2, así que ejecuté con -XX: UseAVX = 1): -XX: + UseSuperWord (con ''-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
¡Diviértete atacando el castillo?
Puedes escribir el kernel de OpenCl para hacer la computación y ejecutarla desde java http://www.jocl.org/ .
El código se puede ejecutar en la CPU y / o GPU y el lenguaje OpenCL también admite tipos de vectores, por lo que debería poder aprovechar explícitamente, por ejemplo, las instrucciones SSE3 / 4.
Supongo que escribió esta pregunta antes de enterarse de netlib-java ;-) proporciona exactamente la API nativa que necesita, con implementaciones optimizadas para la máquina, y no tiene ningún costo en el límite nativo gracias a la fijación de memoria.