¿Algún compilador JIT de JVM genera código que utiliza instrucciones de punto flotante vectorizadas?

95

Digamos que el cuello de botella de mi programa Java son realmente algunos bucles ajustados para calcular un montón de productos punto vectoriales. Sí, he perfilado, sí, es el cuello de botella, sí, es significativo, sí, así es como es 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 productos por pares. 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 estos escribiendo código nativo en JNI. La llamada JNI resulta bastante cara.

Sé que no puede garantizar lo que compilará o no compilará un JIT. ¿Alguien ha oído hablar de un código de generación JIT que utilice estas instrucciones? y si es así, ¿hay algo en el código Java que ayude a que sea compilable de esta manera?

Probablemente un "no"; vale la pena preguntar.

Sean Owen
fuente
4
La forma más fácil de averiguarlo es probablemente obtener el JIT más moderno que pueda encontrar y hacer que genere el ensamblaje generado -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation. Necesitará un programa que ejecute el método vectorizable suficientes veces para hacerlo "caliente".
Louis Wasserman
1
O echa un vistazo a la fuente. download.java.net/openjdk/jdk7
Proyecto de ley
1
"Próximamente" en un jdk cercano a usted: mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2012-July/…
Jonathan S. Fisher
3
En realidad, según este blog , JNI puede ser bastante rápido si se usa "correctamente".
ziggystar
2
Puede encontrar una publicación de blog relevante sobre esto aquí: psy-lob-saw.blogspot.com/2015/04/… con el mensaje general de que la vectorización puede ocurrir, y ocurre. Además de vectorizar casos específicos (Arrays.fill () / equals (char []) / arrayCopy), la JVM se auto-vectoriza usando la Paralelización de Nivel de Superpalabra. El código relevante está en superword.cpp y el documento en el que se basa está aquí: groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf
Nitsan Wakart

Respuestas:

44

Entonces, básicamente, desea que su código se ejecute más rápido. JNI es la respuesta. Sé que dijiste que no te funcionó, 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 @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");
    }
}

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 este comando:

$ java -jar javacpp.jar Dot.java -exec

Con una CPU Intel (R) Core (TM) i7-7700HQ a 2.80GHz, Fedora 30, GCC 9.1.1 y OpenJDK 8 u 11, obtengo este tipo de salida:

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

O aproximadamente 2,4 veces más rápido. Necesitamos utilizar búferes NIO directos en lugar de matrices, pero HotSpot puede acceder a búferes NIO directos tan rápido como matrices . Por otro lado, desenrollar manualmente el lazo no proporciona un aumento mensurable en el rendimiento, en este caso.

Samuel Audet
fuente
3
¿Usó OpenJDK u Oracle HotSpot? Contrariamente a la creencia popular, no son lo mismo.
Jonathan S. Fisher
@exabrial Esto es lo que "java -version" devuelve en esta máquina en este momento: versión java "1.6.0_22" Entorno de tiempo de ejecución OpenJDK (IcedTea6 1.10.6) (fedora-63.1.10.6.fc15-x86_64) VM de servidor OpenJDK de 64 bits (compilación 20.0-b11, modo mixto)
Samuel Audet
1
Es probable que ese bucle tenga una dependencia de bucle transportado. Puede obtener una mayor aceleración desenrollando el bucle dos o más veces.
3
@Oliv GCC vectoriza el código con SSE, sí, pero para datos tan pequeños, la sobrecarga de llamadas JNI es, lamentablemente, demasiado grande.
Samuel Audet
2
En mi A6-7310 con JDK 13, obtengo: dot (): 69 ns / dotc (): 95 ns. ¡Java gana!
Stefan Reich
39

Para abordar parte del escepticismo expresado por otros aquí, sugiero que cualquiera que quiera probarse a sí mismo oa otros use el siguiente método:

  • Crea un proyecto JMH
  • Escribe un pequeño fragmento de matemáticas vectorizables.
  • Ejecute su punto de referencia cambiando entre -XX: -UseSuperWord y -XX: + UseSuperWord (predeterminado)
  • Si no se observa ninguna diferencia en el rendimiento, es probable que su código no se haya vectorizado
  • Para asegurarse, ejecute su punto de referencia de manera que imprima el ensamblaje. En Linux, puede disfrutar del perfasm profiler ('- prof perfasm'). Eche un vistazo y vea si se generan las instrucciones que espera.

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 el indicador (en la computadora portátil Haswell reciente, Oracle JDK 8u60): -XX: + UseSuperWord: 475.073 ± 44.579 ns / op (nanosegundos por operación) -XX: -UseSuperWord: 3376.364 ± 233.211 ns / op

El ensamblaje para el bucle activo es un poco difícil de formatear y pegar aquí, pero aquí hay un fragmento (hsdis.so no puede formatear algunas de las instrucciones vectoriales 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?

Nitsan Wakart
fuente
1
Del mismo artículo: "El resultado del desensamblador JITed sugiere que en realidad no es tan eficiente en términos de llamar a las instrucciones SIMD más óptimas y su programación. Una búsqueda rápida a través del código fuente del compilador JVM JIT (Hotspot) sugiere que esto se debe a la inexistencia de códigos de instrucciones SIMD empaquetados ". Los registros SSE se utilizan en modo escalar.
Aleksandr Dubinsky
1
@AleksandrDubinsky, algunos casos están cubiertos, otros no. ¿Tiene un caso concreto que le interese?
Nitsan Wakart
2
Cambiemos la pregunta y preguntemos si la JVM autovectorizará las operaciones aritméticas. ¿Puede dar un ejemplo? Tengo un bucle que tuve que sacar y reescribir usando intrínsecos recientemente. Sin embargo, en lugar de esperar la autovectorización, me gustaría ver soporte para vectorización / intrínsecos explícitos (similar a agner.org/optimize/vectorclass.pdf ). Aún mejor sería escribir un buen backend de Java para Aparapi (aunque el liderazgo de ese proyecto tiene algunos objetivos equivocados). ¿Trabajas en la JVM?
Aleksandr Dubinsky
1
@AleksandrDubinsky Espero que la respuesta ampliada ayude, si no, tal vez un correo electrónico lo haría. También tenga en cuenta que "reescribir usando intrínsecos" implica que cambió el código JVM para agregar nuevos intrínsecos, ¿es eso lo que quiere decir? Supongo que se refería a reemplazar su código Java con llamadas en una implementación nativa a través de JNI
Nitsan Wakart
1
Gracias. Esta debería ser ahora la respuesta oficial. Creo que debería eliminar la referencia al documento, ya que está desactualizado y no demuestra vectorización.
Aleksandr Dubinsky
26

En las versiones de HotSpot que comienzan con Java 7u40, el compilador del servidor brinda soporte para la vectorización automática. Según JDK-6340864

Sin embargo, esto parece ser cierto solo para los "bucles simples", al menos por el momento. Por ejemplo, la acumulación de una matriz aún no se puede vectorizar JDK-7192383

Vedrán
fuente
La vectorización también existe en JDK6 en algunos casos, aunque el conjunto de instrucciones SIMD objetivo no es tan amplio.
Nitsan Wakart
3
El soporte de vectorización del compilador en HotSpot mejoró mucho últimamente (junio de 2017) debido a las contribuciones de Intel. En cuanto al rendimiento, el jdk9 aún inédito (b163 y posterior) actualmente gana sobre jdk8 debido a las correcciones de errores que habilitan AVX2. Los bucles deben cumplir algunas restricciones para que funcione la autovectorización, por ejemplo, use: contador int, incremento de contador constante, una condición de terminación con variables invariantes de bucle, cuerpo del bucle sin llamadas a métodos (?), ¡Sin despliegue manual del bucle! Los detalles están disponibles en: cr.openjdk.java.net/~vlivanov/talks/…
Vedran
El soporte vectorizado fusionado-múltiple-agregado (FMA) no se ve bien actualmente (a junio de 2017): es vectorización o FMA escalar (?). Sin embargo, aparentemente Oracle acaba de aceptar la contribución de Intel al HotSpot que permite la vectorización FMA usando AVX-512. Para el deleite de los fanáticos de la autovectorización y los afortunados de tener acceso al hardware AVX-512, esto puede aparecer (con algo de suerte) en una de las próximas compilaciones de EA jdk9 (más allá de b175).
Vedran
Un enlace para respaldar la declaración anterior (RFR (M): 8181616: Vectorización FMA en x86): mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/…
Vedran
2
Un pequeño punto de referencia que demuestra la aceleración por un factor de 4 en enteros a través de la vectorización de bucle utilizando instrucciones AVX2: prestodb.rocks/code/simd
Vedran
6

Aquí hay un buen artículo sobre cómo experimentar con Java y las instrucciones SIMD escritas por mi amigo: http://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 debes esperar mucho y debes tener cuidado.

kokosing
fuente
1
Sería útil si resumiera algunas ideas clave del artículo al que se vinculó.
Aleksandr Dubinsky
4

Puede escribir el kernel OpenCl para hacer la computación y ejecutarlo desde java http://www.jocl.org/ .

El código se puede ejecutar en CPU y / o GPU y el lenguaje OpenCL también admite tipos de vector, por lo que debería poder aprovechar explícitamente, por ejemplo, las instrucciones SSE3 / 4.

Mikael Lepistö
fuente
4

Eche un vistazo a la comparación de rendimiento entre Java y JNI para una implementación óptima de micro-kernels computacionales . Muestran que el compilador del servidor Java HotSpot VM admite la vectorización automática mediante el paralelismo de nivel de superpalabra, que se limita a casos simples de paralelismo dentro del ciclo. Este artículo también le dará una guía sobre si el tamaño de sus datos es lo suficientemente grande como para justificar la ruta JNI.

Paul Jurczak
fuente
3

Supongo que escribió esta pregunta antes de descubrir 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 debido a la fijación de memoria.

fommil
fuente
1
Sí, hace mucho tiempo. Tenía más esperanzas de escuchar que esto se traduce automágicamente a instrucciones vectorizadas. Pero claramente no es tan difícil hacer que suceda manualmente.
Sean Owen
-4

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 cambiar en lugar de multiplicar cuando se trata de una potencia de dos. El proyecto mono introdujo su propio vector y otros métodos con respaldo nativo para ayudar al rendimiento.

mP.
fuente
3
Actualmente, ningún compilador de hotspot de Java hace esto, pero no es mucho más difícil que las cosas que hacen. Usan instrucciones SIMD para copiar múltiples valores de matriz a la vez. Solo tiene que escribir más código de generación de código y coincidencia de patrones, lo cual es bastante sencillo después de desenrollar un poco el bucle. Creo que la gente de Sun se volvió perezosa, pero parece que ahora sucederá en Oracle (¡yay Vladimir! ¡Esto debería ayudar mucho a nuestro código!): Mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ …
Christopher Manning