[...] (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 Pixel
objeto (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 Pixel
objeto, utiliza una matriz de bytes y modela un Image
objeto. 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 int
lugar de Integer
) y comience a modelar interfaces masivas como un Image
interfaz, no Pixel
interfaz. 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 int
garantías garantiza una representación contigua. Una serie de Integer
no. 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 Integer
puede 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 rodeanIntegers
estaban 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 Integer
ser 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 Boolean
es 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 final
para 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 Integer
vs. int
puede 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 int
vs. 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 ints
versus un millón aleatorio Integers
y hacer esto repetidamente ( Integers
se 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 Account
si 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.