¿Es Java mucho más difícil de "ajustar" para el rendimiento en comparación con C / C ++? [cerrado]

11

¿La "magia" de la JVM obstaculiza la influencia que tiene un programador sobre las micro optimizaciones en Java? Recientemente leí en C ++ que a veces la ordenación de los miembros de datos puede proporcionar optimizaciones (concedidas, en el entorno de microsegundos) y supuse que las manos de un programador están atadas cuando se trata de exprimir el rendimiento de Java.

Aprecio que un algoritmo decente proporcione mayores ganancias de velocidad, pero una vez que tiene el algoritmo correcto, ¿es más difícil ajustar Java debido al control JVM?

De lo contrario, ¿podrían las personas dar ejemplos de los trucos que puede usar en Java (además de simples indicadores de compilación).

usuario997112
fuente
14
El principio básico detrás de toda optimización de Java es este: la JVM probablemente ya lo haya hecho mejor que usted. La optimización consiste principalmente en seguir prácticas de programación sensatas y evitar las cosas habituales, como concatenar cadenas en un bucle.
Robert Harvey
3
El principio de la microoptimización en todos los idiomas es que el compilador ya lo hizo mejor que usted. El otro principio de la microoptimización en todos los idiomas es que poner más hardware en él es más barato que el tiempo que el programador microoptimiza. El programador tiene que tender a problemas de escala (algoritmos subóptimos), pero la microoptimización es una pérdida de tiempo. A veces, la microoptimización tiene sentido en sistemas embebidos en los que no se puede agregar más hardware, pero Android con Java, y una implementación bastante pobre, muestra que la mayoría de ellos ya tienen suficiente hardware.
Jan Hudec
1
los "trucos de rendimiento de Java" que vale la pena estudiar son: Java eficaz , Angelika Langer Enlaces - Artículos relacionados con el rendimiento y el rendimiento de Java de Brian Goetz en la teoría y práctica de Java y la serie Threading Lightly enumerados aquí
mosquito
2
Tenga mucho cuidado con los consejos y trucos: la JVM, los sistemas operativos y el hardware avanzan; es mejor que aprenda la metodología de ajuste del rendimiento y aplique mejoras para su entorno particular :-)
Martijn Verburg
En algunos casos, una VM puede hacer optimizaciones en tiempo de ejecución que no son prácticas en tiempo de compilación. El uso de la memoria administrada puede mejorar el rendimiento, aunque a menudo también tendrá una mayor huella de memoria. La memoria no utilizada se libera cuando es conveniente, en lugar de lo antes posible.
Brian

Respuestas:

5

Claro, en el nivel de microoptimización, la JVM hará algunas cosas sobre las que tendrá poco control en comparación con C y C ++ especialmente.

Por otro lado, la variedad de comportamientos del compilador con C y C ++ especialmente tendrá un impacto negativo mucho mayor en su capacidad para realizar microoptimizaciones de cualquier manera vagamente portátil (incluso en las revisiones del compilador).

Depende de qué tipo de proyecto esté ajustando, a qué entornos se dirija, etc. Y al final, realmente no importa, ya que está obteniendo unos pocos órdenes de magnitud mejores resultados de las optimizaciones algorítmicas / de estructura de datos / diseño de programas de todos modos.

Telastyn
fuente
Puede importar mucho cuando descubres que tu aplicación no se escala en todos los núcleos
James
@james - ¿quieres elaborar?
Telastyn
1
@James, escalar los núcleos tiene muy poco que ver con el lenguaje de implementación (¡excepto Python!), Y mucho más que ver con la arquitectura de la aplicación.
James Anderson el
29

Las micro optimizaciones casi nunca valen la pena, y los compiladores y los tiempos de ejecución realizan casi todas las tareas fáciles automáticamente.

Sin embargo, hay un área importante de optimización en la que C ++ y Java son fundamentalmente diferentes, y es el acceso a memoria masiva. C ++ tiene administración de memoria manual, lo que significa que puede optimizar el diseño de datos de la aplicación y los patrones de acceso para hacer un uso completo de los cachés. Esto es bastante difícil, algo específico para el hardware en el que se está ejecutando (por lo que las ganancias de rendimiento pueden desaparecer en hardware diferente), pero si se hace correctamente, puede conducir a un rendimiento absolutamente impresionante. Por supuesto, pagas por ello con la posibilidad de todo tipo de errores horribles.

Con un lenguaje recolectado como basura, Java, este tipo de optimizaciones no se pueden hacer en el código. Algunos se pueden hacer en tiempo de ejecución (automáticamente o mediante la configuración, ver más abajo), y otros simplemente no son posibles (el precio que paga por estar protegido contra errores de administración de memoria).

De lo contrario, ¿podrían las personas dar ejemplos de los trucos que puede usar en Java (además de simples indicadores de compilación).

Los indicadores del compilador son irrelevantes en Java porque el compilador de Java casi no optimiza; el tiempo de ejecución lo hace.

Y, de hecho, los tiempos de ejecución de Java tienen una multitud de parámetros que se pueden ajustar, especialmente en relación con el recolector de basura. No hay nada "simple" en esas opciones: los valores predeterminados son buenos para la mayoría de las aplicaciones, y para obtener un mejor rendimiento es necesario que comprenda exactamente qué hacen las opciones y cómo se comporta su aplicación.

Michael Borgwardt
fuente
1
+1: básicamente lo que estaba escribiendo en mi respuesta, tal vez una mejor formulación.
Klaim
1
+1: Muy buenos puntos, explicados de una manera muy concisa: "Esto es bastante difícil ... pero si se hace bien, puede conducir a un rendimiento absolutamente impresionante. Por supuesto, se paga con la posibilidad de todo tipo de errores horribles ".
Giorgio
1
@ MartinBa: Es más que paga por optimizar la administración de memoria. Si no intenta optimizar la administración de la memoria, la administración de la memoria C ++ no es tan difícil (evítela completamente a través de STL o hágalo relativamente fácil usando RAII). Por supuesto, implementar RAII en C ++ requiere más líneas de código que no hacer nada en Java (es decir, porque Java lo maneja por usted).
Brian
3
@ Martin Ba: Básicamente sí. Punteros colgantes, desbordamientos de búfer, punteros no inicializados, errores en la aritmética de punteros, todo lo que simplemente no existe sin la gestión manual de la memoria. Y para optimizar el acceso a la memoria es necesario que haga mucha gestión manual de la memoria.
Michael Borgwardt
1
Hay un par de cosas que puedes hacer en Java. Una es la agrupación de objetos, que maximiza las posibilidades de la localidad de memoria de los objetos (a diferencia de C ++, donde puede garantizar la localidad de memoria).
RokL
5

[...] (concedido, en el entorno de microsegundos) [...]

Los microsegundos se suman si estamos pasando de millones a miles de millones de cosas. Una sesión personal de vtune / micro-optimización de C ++ (sin mejoras algorítmicas):

T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds

Todo, además de "multihilo", "SIMD" (escrito a mano para vencer al compilador) y la optimización del parche de 4 valencia, eran optimizaciones de memoria a nivel micro. Además, el código original a partir de los tiempos iniciales de 32 segundos ya estaba bastante optimizado (complejidad algorítmica teóricamente óptima) y esta es una sesión reciente. La versión original mucho antes de esta sesión reciente tardó más de 5 minutos en procesarse.

La optimización de la eficiencia de la memoria puede ayudar a menudo desde varias veces hasta órdenes de magnitud en un contexto de subproceso único y más en contextos multiproceso (los beneficios de un representante de memoria eficiente a menudo se multiplican con múltiples subprocesos en la mezcla).

Sobre la importancia de la microoptimización

Me inquieta un poco esta idea de que las micro optimizaciones son una pérdida de tiempo. Estoy de acuerdo en que es un buen consejo general, pero no todos lo hacen incorrectamente en base a corazonadas y supersticiones en lugar de mediciones. Hecho correctamente, no necesariamente produce un micro impacto. Si tomamos el propio Embree (núcleo de trazado de rayos) de Intel y probamos solo el BVH escalar simple que han escrito (no el paquete de rayos que es exponencialmente más difícil de superar), y luego intentamos superar el rendimiento de esa estructura de datos, puede ser muy útil. experiencia humilde incluso para un veterano acostumbrado a perfilar y ajustar el código durante décadas. Y todo se debe a las micro optimizaciones aplicadas. Su solución puede procesar más de cien millones de rayos por segundo cuando he visto profesionales industriales trabajando en trazado de rayos que pueden '

No hay forma de llevar a cabo una implementación directa de un BVH con solo un enfoque algorítmico y obtener más de cien millones de intersecciones de rayos primarios por segundo con cualquier compilador optimizador (incluso el propio ICC de Intel). Una sencilla a menudo ni siquiera recibe un millón de rayos por segundo. Se necesitan soluciones de calidad profesional para obtener incluso algunos millones de rayos por segundo. Se necesita micro-optimización de nivel Intel para obtener más de cien millones de rayos por segundo.

Algoritmos

Creo que la microoptimización no es importante siempre que el rendimiento no sea importante a nivel de minutos a segundos, por ejemplo, u horas a minutos. Si tomamos un algoritmo horrible como el ordenamiento de burbujas y lo usamos sobre una entrada masiva como ejemplo, y luego lo comparamos incluso con una implementación básica de ordenamiento por fusión, el primero puede tardar meses en procesarse, el último tal vez 12 minutos, como resultado de complejidad cuadrática vs linealitmica.

La diferencia entre meses y minutos probablemente hará que la mayoría de las personas, incluso aquellas que no trabajan en campos críticos para el rendimiento, consideren que el tiempo de ejecución es inaceptable si requiere que los usuarios esperen meses para obtener un resultado.

Mientras tanto, si comparamos la ordenación de fusión directa no micro-optimizada con la ordenación rápida (que no es en absoluto algorítmicamente superior a la ordenación de fusión, y solo ofrece mejoras a nivel micro para la localidad de referencia), la ordenación rápida micro-optimizada podría terminar en 15 segundos en lugar de 12 minutos. Hacer que los usuarios esperen 12 minutos puede ser perfectamente aceptable (tiempo de descanso para tomar café).

Creo que esta diferencia es probablemente insignificante para la mayoría de las personas entre, digamos, 12 minutos y 15 segundos, y es por eso que la micro-optimización a menudo se considera inútil, ya que a menudo solo es como la diferencia entre minutos y segundos, y no minutos y meses. La otra razón por la que creo que se considera inútil es que a menudo se aplica a áreas que no importan: alguna pequeña área que ni siquiera es irregular y crítica que produce una diferencia cuestionable del 1% (que muy bien podría ser solo ruido). Pero para las personas que se preocupan por este tipo de diferencias de tiempo y están dispuestas a medir y hacerlo bien, creo que vale la pena prestar atención al menos a los conceptos básicos de la jerarquía de memoria (específicamente los niveles superiores relacionados con fallas de página y errores de caché) .

Java deja mucho espacio para buenas micro optimizaciones

Uf, lo siento, con ese tipo de despotricar a un lado:

¿La "magia" de la JVM obstaculiza la influencia que tiene un programador sobre las micro optimizaciones en Java?

Un poco, pero no tanto como la gente podría pensar si lo haces bien. Por ejemplo, si está procesando imágenes, en código nativo con SIMD manuscrita, multiprocesamiento y optimizaciones de memoria (patrones de acceso y posiblemente incluso representación dependiendo del algoritmo de procesamiento de imágenes), es fácil procesar cientos de millones de píxeles por segundo durante 32- Píxeles RGBA (canales de color de 8 bits) y, a veces, incluso miles de millones por segundo.

Es imposible acercarse a Java si dice que hizo un Pixelobjeto (esto solo inflaría el tamaño de un píxel de 4 bytes a 16 en 64 bits).

Pero es posible que pueda acercarse mucho más si evita el Pixelobjeto, utiliza una matriz de bytes y modela un Imageobjeto. Java sigue siendo bastante competente allí si comienzas a usar matrices de datos antiguos simples. He intentado este tipo de cosas antes en Java y me impresionó bastante, siempre y cuando no crees un montón de pequeños objetos en todas partes que sean 4 veces más grandes de lo normal (por ejemplo: use en intlugar de Integer) y comience a modelar interfaces masivas como un Imageinterfaz, no Pixelinterfaz. Incluso me atrevería a decir que Java puede competir con el rendimiento de C ++ si está recorriendo datos antiguos y no objetos (grandes matrices de float, por ejemplo, no Float).

Quizás aún más importante que los tamaños de memoria es que una serie de intgarantías garantiza una representación contigua. Una serie de Integerno. La contigüidad es a menudo esencial para la localidad de referencia, ya que significa que múltiples elementos (ej .: 16 ints) pueden caber en una sola línea de caché y potencialmente acceder a ellos juntos antes del desalojo con patrones eficientes de acceso a la memoria. Mientras tanto, un solo Integerpuede quedar varado en algún lugar de la memoria, ya que la memoria circundante es irrelevante, solo para que esa región de memoria se cargue en una línea de caché solo para usar un solo entero antes del desalojo en lugar de 16 enteros. Incluso si tenemos una suerte maravillosa y nos rodeanIntegersestaban bien uno al lado del otro en la memoria, solo podemos caber 4 en una línea de caché a la que se puede acceder antes del desalojo como resultado de Integerser 4 veces más grande, y eso es en el mejor de los casos.

Y hay muchas micro optimizaciones que se pueden tener allí ya que estamos unificados bajo la misma arquitectura / jerarquía de memoria. Los patrones de acceso a la memoria no importan, sin importar el lenguaje que use, los conceptos como el mosaico / bloqueo de bucles generalmente se aplican con mucha más frecuencia en C o C ++, pero benefician a Java de la misma manera.

Recientemente leí en C ++ a veces el orden de los miembros de datos puede proporcionar optimizaciones [...]

El orden de los miembros de datos generalmente no importa en Java, pero eso es principalmente algo bueno. En C y C ++, preservar el orden de los miembros de datos a menudo es importante por razones ABI, por lo que los compiladores no se meten con eso. Los desarrolladores humanos que trabajan allí deben tener cuidado de hacer cosas como organizar sus miembros de datos en orden descendente (de mayor a menor) para evitar desperdiciar memoria en el relleno. Con Java, aparentemente el JIT puede reordenar los miembros sobre la marcha para garantizar una alineación adecuada mientras minimiza el relleno, por lo que, siempre que sea así, automatiza algo que los programadores promedio de C y C ++ a menudo pueden hacer mal y terminan desperdiciando memoria de esa manera ( que no solo es desperdiciar memoria, sino que a menudo desperdicia velocidad aumentando el paso entre las estructuras de AoS innecesariamente y causando más errores de caché). Eso' Es muy robótico reorganizar los campos para minimizar el relleno, por lo que idealmente los humanos no se ocupan de eso. El único momento en que la disposición de los campos puede ser importante de una manera que requiera que un humano conozca la disposición óptima es si el objeto es mayor que 64 bytes y estamos organizando los campos según el patrón de acceso (no el relleno óptimo), en cuyo caso podría ser un esfuerzo más humano (requiere comprender rutas críticas, parte de la cual es información que un compilador no puede anticipar sin saber qué harán los usuarios con el software).

De lo contrario, ¿podrían las personas dar ejemplos de los trucos que puede usar en Java (además de simples indicadores de compilación).

La mayor diferencia para mí en términos de una mentalidad optimizadora entre Java y C ++ es que C ++ podría permitirle usar objetos un poco (más) un poco más que Java en un escenario de rendimiento crítico. Por ejemplo, C ++ puede ajustar un número entero a una clase sin ningún tipo de sobrecarga (referencia en todo el lugar). Java debe tener esa metadata estilo puntero + relleno de alineación sobrecarga por objeto, por eso Booleanes más grande que boolean(pero a cambio proporciona beneficios uniformes de reflexión y la capacidad de anular cualquier función que no esté marcada como finalpara cada UDT individual).

Es un poco más fácil en C ++ controlar la contigüidad de los diseños de memoria en campos no homogéneos (por ejemplo, entrelazar flotadores e ints en una matriz a través de una estructura / clase), ya que la localidad espacial a menudo se pierde (o al menos se pierde el control) en Java al asignar objetos a través del GC.

... pero a menudo las soluciones de mayor rendimiento a menudo las dividirán de todos modos y usarán un patrón de acceso SoA sobre matrices contiguas de datos antiguos simples. Por lo tanto, para las áreas que necesitan un rendimiento máximo, las estrategias para optimizar el diseño de la memoria entre Java y C ++ son a menudo las mismas, y a menudo lo harán demoler esas pequeñas interfaces orientadas a objetos en favor de las interfaces de estilo de colección que pueden hacer cosas como hot / división en campo frío, repeticiones de SoA, etc. Las repeticiones de AoSoA no homogéneas parecen un poco imposibles en Java (a menos que haya utilizado una matriz cruda de bytes o algo así), pero esos son para casos raros donde amboslos patrones de acceso secuencial y aleatorio deben ser rápidos y, al mismo tiempo, tener una combinación de tipos de campo para campos calientes. Para mí, la mayor parte de la diferencia en la estrategia de optimización (en el tipo general de nivel) entre estos dos es discutible si está alcanzando el máximo rendimiento.

Las diferencias varían un poco más si simplemente está buscando un "buen" rendimiento: no poder hacer tanto con objetos pequeños como Integervs. intpuede ser un poco más PITA, especialmente con la forma en que interactúa con los genéricos . Es un poco más difícil Sólo construir una estructura de datos genérica como objetivo la optimización central en Java que funciona para int, float, etc., evitando aquellas UDT más grandes y caros, pero a menudo las zonas más críticas para el desempeño requerirá mano de laminación en sus propias estructuras de datos sintonizado para un propósito muy específico de todos modos, por lo que solo es molesto para el código que se esfuerza por obtener un buen rendimiento pero no un rendimiento máximo.

Objeto de arriba

Tenga en cuenta que la sobrecarga de objetos Java (metadatos y pérdida de localidad espacial y pérdida temporal de localidad temporal después de un ciclo inicial de GC) a menudo es grande para cosas que son realmente pequeñas (como intvs. Integer) que están siendo almacenadas por millones en alguna estructura de datos que es en gran parte contigua y se accede en bucles muy apretados. Parece haber mucha sensibilidad sobre este tema, por lo que debo aclarar que no debe preocuparse por la sobrecarga de objetos para objetos grandes como imágenes, solo objetos realmente minúsculos como un solo píxel.

Si alguien se siente dudoso sobre esta parte, sugeriría hacer un punto de referencia entre sumar un millón aleatorio intsversus un millón aleatorio Integersy hacer esto repetidamente ( Integersse reorganizará en la memoria después de un ciclo GC inicial).

Último truco: diseños de interfaz que dejan espacio para optimizar

Entonces, el mejor truco de Java, según lo veo, si se trata de un lugar que maneja una carga pesada sobre objetos pequeños (por ejemplo: a Pixel, un vector 4, una matriz 4x4, a Particle, posiblemente incluso Accountsi solo tiene unos pocos campos) es evitar el uso de objetos para estas cosas pequeñas y usar matrices (posiblemente encadenados) de datos antiguos simples. Los objetos se convierten entonces en las interfaces de colección como Image, ParticleSystem, Accounts, una colección de matrices o vectores, etc. las individuales se puede acceder mediante un índice, por ejemplo, Este es también uno de los últimos trucos de diseño en C y C ++, ya que incluso sin que los gastos generales objeto básico y memoria desarticulada, modelar la interfaz al nivel de una sola partícula impide las soluciones más eficientes.

ChrisF
fuente
1
Teniendo en cuenta que el mal rendimiento en general puede tener una posibilidad decente de un rendimiento máximo abrumador en las áreas críticas, no creo que uno pueda ignorar por completo la ventaja de tener un buen rendimiento fácilmente. Y el truco de convertir una matriz de estructuras en una estructura de matrices se rompe de alguna manera cuando se accede a todos (o casi todos) los valores que comprenden una de las estructuras originales al mismo tiempo. Por cierto: veo que estás desenterrando muchas publicaciones antiguas y agregando tu propia buena respuesta, a veces incluso la buena respuesta ;-)
Deduplicator
1
@Dupuplicator ¡Espero no molestar a la gente golpeándome demasiado! Este se puso un poco chiflado, tal vez debería mejorarlo un poco. SoA vs. AoS a menudo es difícil para mí (acceso secuencial versus acceso aleatorio). Raramente sé cuál debo usar por adelantado, ya que a menudo hay una mezcla de acceso secuencial y aleatorio en mi caso. La valiosa lección que aprendí a menudo es diseñar interfaces que dejen suficiente espacio para jugar con la representación de datos, interfaces un poco más voluminosas que tienen grandes algoritmos de transformación cuando es posible (a veces no es posible con pequeños bits de acceso aleatorio aquí y allá).
1
Bueno, solo me di cuenta porque las cosas son realmente lentas. Y me tomé mi tiempo con cada uno.
Deduplicador
Realmente me pregunto por qué se user204677fue. Qué gran respuesta.
oligofren
3

Hay un área intermedia entre la microoptimización, por un lado, y la buena elección del algoritmo, por el otro.

Es el área de aceleraciones de factor constante, y puede producir órdenes de magnitud.
La forma en que lo hace es cortando fracciones enteras del tiempo de ejecución, como primero el 30%, luego el 20% de lo que queda, luego el 50% de eso, y así sucesivamente durante varias iteraciones, hasta que casi no quede nada.

No ves esto en pequeños programas de estilo demo. Donde lo ve es en grandes programas serios con muchas estructuras de datos de clase, donde la pila de llamadas suele tener muchas capas de profundidad. Una buena manera de encontrar las oportunidades de aceleración es examinando muestras de tiempo aleatorio del estado del programa.

En general, las aceleraciones consisten en cosas como:

  • minimizando las llamadas newagrupando y reutilizando objetos antiguos,

  • Reconociendo las cosas que se están haciendo allí, por el bien de la generalidad, en lugar de ser realmente necesarias,

  • revisando la estructura de datos mediante el uso de diferentes clases de recopilación que tienen el mismo comportamiento big-O pero aprovechan los patrones de acceso realmente utilizados,

  • guardar datos que han sido adquiridos por llamadas a funciones en lugar de volver a llamar a la función (es una tendencia natural y divertida de los programadores suponer que las funciones que tienen nombres más cortos se ejecutan más rápido).

  • tolerar una cierta cantidad de inconsistencia entre las estructuras de datos redundantes, en lugar de tratar de mantenerlas completamente consistentes con los eventos de notificación,

  • etcétera etcétera.

Pero, por supuesto, ninguna de estas cosas debe hacerse sin antes demostrar que se trata de problemas al tomar muestras.

Mike Dunlavey
fuente
2

Java (hasta donde yo sé) no le da control sobre las ubicaciones variables en la memoria, por lo que le resulta más difícil evitar cosas como el intercambio falso y la alineación de variables (puede rellenar una clase con varios miembros no utilizados). Otra cosa que no creo que pueda aprovechar son instrucciones como mmpause, pero estas cosas son específicas de la CPU, por lo que si cree que lo necesita, Java puede no ser el lenguaje para usar.

Existe la clase insegura que le brinda flexibilidad de C / C ++ pero también con el peligro de C / C ++.

Podría ayudarlo a mirar el código de ensamblaje que genera la JVM para su código

Para leer sobre una aplicación Java que analiza este tipo de detalles, consulte el código de disruptor publicado por LMAX

James
fuente
2

Esta pregunta es muy difícil de responder, porque depende de las implementaciones de lenguaje.

En general, hay muy poco espacio para tales "micro optimizaciones" en estos días. La razón principal es que los compiladores aprovechan tales optimizaciones durante la compilación. Por ejemplo, no hay diferencia de rendimiento entre los operadores anteriores y posteriores al incremento en situaciones donde su semántica es idéntica. Otro ejemplo sería, por ejemplo, un ciclo como este for(int i=0; i<vec.size(); i++)donde uno podría argumentar que en lugar de llamar alsize()función de miembro durante cada iteración, sería mejor obtener el tamaño del vector antes del bucle y luego compararlo con esa única variable y así evitar la función de una llamada por iteración. Sin embargo, hay casos en los que un compilador detectará este caso tonto y almacenará en caché el resultado. Sin embargo, esto solo es posible cuando la función no tiene efectos secundarios y el compilador puede estar seguro de que el tamaño del vector permanece constante durante el ciclo, por lo que simplemente se aplica a casos bastante triviales.

zxcdw
fuente
En cuanto al segundo caso, no creo que el compilador pueda optimizarlo en el futuro previsible. La detección de que es seguro optimizar vec.size () depende de probar que el tamaño si el vector / perdido no cambia dentro del ciclo, lo que creo que es indecidible debido al problema de detención.
Lie Ryan
@LieRyan He visto casos múltiples (simples) en los que el compilador ha generado un archivo binario exactamente idéntico si el resultado se ha "almacenado en caché" manualmente y se ha llamado a size (). Escribí un código y resulta que el comportamiento depende mucho de la forma en que funciona el programa. Hay casos en los que el compilador puede garantizar que no hay posibilidad de que cambie el tamaño del vector durante el ciclo, y luego hay casos en los que no puede garantizarlo, muy similar al problema de detención como mencionó. Por ahora no puedo verificar mi reclamo (el desmontaje de C ++ es una molestia), así que
edité
2
@Lie Ryan: muchas cosas que son indecidibles en el caso general son perfectamente decidibles para casos específicos pero comunes, y eso es realmente todo lo que necesitas aquí.
Michael Borgwardt
@LieRyan Si solo llama a constmétodos en este vector, estoy bastante seguro de que muchos compiladores de optimización lo resolverán.
K.Steff
en C #, y creo que también leí en Java, si no almacena el tamaño de la memoria caché, el compilador sabe que puede eliminar las verificaciones para ver si va fuera de los límites de la matriz, y si lo hace, tiene que hacer las verificaciones , que generalmente cuestan más de lo que ahorra al almacenar en caché. Intentar burlar a los optimizadores rara vez es un buen plan.
Kate Gregory
1

¿Podría la gente dar ejemplos de qué trucos puedes usar en Java (además de simples indicadores de compilación)?

Además de las mejoras de los algoritmos, asegúrese de considerar la jerarquía de la memoria y cómo la utiliza el procesador. Hay grandes beneficios en la reducción de las latencias de acceso a la memoria, una vez que comprende cómo el lenguaje en cuestión asigna memoria a sus tipos de datos y objetos.

Ejemplo de Java para acceder a una matriz de 1000x1000 ints

Considere el siguiente código de muestra: accede a la misma área de memoria (una matriz de entradas de 1000x1000), pero en un orden diferente. En mi Mac mini (Core i7, 2.7 GHz) la salida es la siguiente, que muestra que atravesar la matriz por filas más que duplica el rendimiento (promedio de más de 100 rondas cada una).

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

Esto se debe a que la matriz se almacena de manera que las columnas consecutivas (es decir, los valores int) se colocan adyacentes en la memoria, mientras que las filas consecutivas no. Para que el procesador realmente use los datos, debe transferirse a sus cachés. La transferencia de memoria se realiza mediante un bloque de bytes, llamado línea de caché ; cargar una línea de caché directamente desde la memoria introduce latencias y, por lo tanto, disminuye el rendimiento de un programa.

Para el Core i7 (puente arenoso), una línea de caché contiene 64 bytes, por lo que cada acceso a la memoria recupera 64 bytes. Debido a que la primera prueba accede a la memoria en una secuencia predecible, el procesador buscará previamente los datos antes de que el programa los consuma realmente. En general, esto da como resultado una menor latencia en los accesos a la memoria y, por lo tanto, mejora el rendimiento.

Código de muestra:

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }
miraculixx
fuente
1

El JVM puede interferir, y a menudo lo hace, y el compilador JIT puede cambiar significativamente entre versiones. Algunas micro optimizaciones son imposibles en Java debido a limitaciones de lenguaje, como ser amigable con el subproceso o la última colección SIMD de los procesadores Intel.

Se recomienda leer un blog altamente informativo sobre el tema de uno de los autores de Disruptor :

Siempre hay que preguntarse por qué molestarse en usar Java si desea micro optimizaciones, existen muchos métodos alternativos para la aceleración de una función, como usar JNA o JNI para pasar a una biblioteca nativa.

Steve-o
fuente