Mi amigo Aras
recientemente escribió el mismo rastreador de rayos en diferentes idiomas, incluidos C ++, C # y el compilador Unity Burst. Por supuesto, es natural esperar que C # sea más lento que C ++, pero me pareció interesante que Mono sea más lento que .NET Core.
Sus
indicadores publicados fueron pobres:
- C # (.NET Core): Mac 17.5 Mray / s,
- C # (Unidad, Mono): Mac 4.6 Mray / s,
- C # (Unity, IL2CPP): Mac 17.1 Mray / s
Decidí ver qué sucedía y documentar los lugares que podrían mejorarse.
Como resultado de este punto de referencia y estudiando este problema, encontramos tres áreas en las que es posible mejorar:
- Primero, necesita mejorar la configuración predeterminada de Mono, porque los usuarios generalmente no configuran sus configuraciones
- En segundo lugar, necesitamos presentar activamente al mundo el back-end de la optimización del código LLVM en Mono
- En tercer lugar, mejoramos el ajuste de algunos parámetros Mono.
El punto de referencia de esta prueba fueron los resultados de la ejecución del
trazador de rayos en mi máquina, y dado que tengo un hardware diferente, no podemos comparar los números.
Los resultados en mi casa iMac para Mono y .NET Core fueron los siguientes:
Ambiente de trabajo | Resultados, MRay / seg |
---|
.NET Core 2.1.4, dotnet run compilación de depuración | 3.6 |
.NET Core 2.1.4 versión build dotnet run -c Release | 21,7 |
Vanilla Mono, mono Maths.exe | 6.6 |
Vanilla Mono con LLVM y float32 | 15,5 |
En el proceso de estudiar este problema, encontramos un par de problemas, después de la corrección de los cuales se obtuvieron los siguientes resultados:
Ambiente de trabajo | Resultados, MRay / seg |
---|
Mono con LLVM y float32 | 15,5 |
Mono avanzado con LLVM, float32 y línea fija | 29,6 |
El panorama general:
Simplemente aplicando LLVM y float32, puede aumentar el rendimiento del código de coma flotante en casi 2.3 veces. Y después del ajuste, que agregamos a Mono como resultado de estos experimentos, puede aumentar la productividad en 4.4 veces en comparación con el Mono estándar: estos parámetros en futuras versiones de Mono se convertirán en los parámetros predeterminados.
En este artículo explicaré nuestros hallazgos.
Flotador de 32 bits y 64 bits
Aras usa números de coma flotante de 32 bits para la parte principal de los cálculos (escriba
float
en C # o
System.Single
en .NET). En Mono, cometimos un error hace mucho tiempo: todos los cálculos de coma flotante de 32 bits se realizaron como 64 bits, y los datos todavía se almacenaban en áreas de 32 bits.
Hoy, mi memoria no es tan aguda como antes, y no puedo recordar exactamente por qué tomamos esa decisión.
Solo puedo suponer que fue influenciado por las tendencias e ideas de la época.
Luego, un aura positiva rondaba la computación flotante con mayor precisión. Por ejemplo, los procesadores Intel x87 utilizaron una precisión de 80 bits para los cálculos de coma flotante, incluso cuando los operandos eran dobles, lo que proporcionó a los usuarios resultados más precisos.
En ese momento, la idea también era relevante de que en uno de mis proyectos anteriores, las hojas de cálculo gnumeric, las funciones estadísticas se implementaron de manera más eficiente que en Excel. Por lo tanto, muchas comunidades son conscientes de la idea de que se pueden utilizar resultados más precisos con mayor precisión.
En las etapas iniciales del desarrollo de Mono, la mayoría de las operaciones matemáticas realizadas en todas las plataformas solo podían recibir el doble en la entrada. Se agregaron versiones de 32 bits a C99, Posix e ISO, pero en aquellos días no estaban ampliamente disponibles para toda la industria (por ejemplo,
sinf
es la versión flotante de
sin
,
fabsf
es la versión de
fabs
, etc.).
En resumen, la década de 2000 fue una época de optimismo.
Las aplicaciones pagaron un alto precio por aumentar el tiempo de cómputo, pero Mono se usó principalmente para aplicaciones Linux de escritorio que sirven páginas HTTP y algunos procesos de servidor, por lo que la velocidad de punto flotante no fue el problema que encontramos a diario. Se hizo notable solo en algunos puntos de referencia científicos, y en 2003 rara vez se desarrollaron en .NET.
Hoy en día, los juegos, las aplicaciones 3D, el procesamiento de imágenes, VR, AR y el aprendizaje automático han convertido las operaciones de punto flotante en un tipo de datos más común. El problema no viene solo, y no hay excepciones. Flotar ya no era el tipo de datos amigable utilizado en el código en solo un par de lugares. Se convirtieron en una avalancha, de la que no hay dónde esconderse. Hay muchos de ellos y su propagación no se puede detener.
Bandera de espacio de trabajo float32
Por lo tanto, hace un par de años decidimos agregar soporte para realizar operaciones flotantes de 32 bits utilizando operaciones de 32 bits, como en todos los demás casos. Llamamos a esta característica del espacio de trabajo "float32". En Mono, se habilita agregando la opción
--O=float32
en el entorno de trabajo, y en las aplicaciones Xamarin este parámetro se cambia en la configuración del proyecto.
Este nuevo indicador fue bien recibido por nuestros usuarios móviles, porque básicamente los dispositivos móviles aún no son demasiado potentes, y es preferible procesar los datos más rápido que tener una mayor precisión. Recomendamos que los usuarios móviles activen el compilador de optimización LLVM y el indicador float32 al mismo tiempo.
Aunque esta bandera se ha implementado durante varios años, no la convertimos en la predeterminada para evitar sorpresas desagradables para los usuarios. Sin embargo, comenzamos a encontrar casos en los que surgen sorpresas debido al comportamiento estándar de 64 bits, consulte este
informe de errores enviado por el usuario de Unity .
Ahora usaremos Mono
float32
, el progreso se puede rastrear aquí:
https://github.com/mono/mono/issues/6985 .
Mientras tanto, volví al proyecto de mi amigo Aras. Utilizó las nuevas API que se agregaron a .NET Core. Aunque .NET Core siempre ha realizado operaciones flotantes de 32 bits como flotantes de 32 bits, la API
System.Math
aún realiza conversiones de
float
a
double
en el proceso. Por ejemplo, si necesita calcular la función seno para un valor flotante, entonces la única opción es llamar a
Math.Sin (double)
, y tendrá que convertir de flotante a doble.
Para solucionar esto,
System.MathF
ha agregado un nuevo tipo de
System.MathF
a .NET Core que contiene operaciones matemáticas con punto flotante de precisión simple, y ahora acabamos de mover esto
[System.MathF]
a Mono .
La transición de flotante de 64 bits a 32 bits mejora significativamente el rendimiento, que se puede ver en esta tabla:
Ambiente de trabajo y opciones | Mray / segundo |
---|
Mono con System.Math | 6.6 |
Mono con System.Math y -O=float32 | 8.1 |
Mono con System.MathF | 6.5 |
Mono con System.MathF y -O=float32 | 8.2 |
Es decir, usar
float32
en esta prueba realmente mejora el rendimiento, y MathF tiene poco efecto.
Configuración LLVM
En el proceso de esta investigación, descubrimos que aunque el compilador Fast JIT Mono tiene soporte
float32
, no agregamos este soporte al back-end LLVM. Esto significaba que Mono con LLVM todavía realizaba costosas conversiones de flotante a doble.
Por lo tanto, Zoltan agregó soporte
float32
al motor de generación de código LLVM.
Luego se dio cuenta de que nuestro inliner utiliza las mismas heurísticas para Fast JIT que las utilizadas para LLVM. Cuando se trabaja con Fast JIT, es necesario lograr un equilibrio entre la velocidad de JIT y la velocidad de ejecución, por lo tanto, limitamos la cantidad de código incrustado para reducir la cantidad de trabajo del motor JIT.
Pero si decide usar LLVM en Mono, entonces se esfuerza por obtener el código lo más rápido posible, por lo que cambiamos la configuración en consecuencia. Hoy, este parámetro se puede cambiar utilizando la
MONO_INLINELIMIT
entorno
MONO_INLINELIMIT
, pero de hecho debe escribirse en los valores predeterminados.
Estos son los resultados con la configuración LLVM modificada:
Ambiente de trabajo y opciones | Mray / segundos |
---|
Mono con System.Math --llvm -O=float32 | 16,0 |
Mono con System.Math --llvm -O=float32 , heurística constante | 29,1 |
Mono con System.MathF --llvm -O=float32 , heurística constante | 29,6 |
Próximos pasos
Se necesitó poco esfuerzo para hacer todas estas mejoras. Estos cambios fueron liderados por discusiones periódicas en Slack. Incluso me las arreglé para hacer unas horas una noche para portar
System.MathF
a Mono.
El código de trazado de rayos de Aras se ha convertido en un tema ideal para estudiar porque era autosuficiente, era una aplicación real y no un punto de referencia sintético. Queremos encontrar otro software similar que pueda usarse para estudiar el código binario que generamos, y asegurarnos de que pasamos LLVM los mejores datos para la ejecución óptima de su trabajo.
También estamos considerando actualizar nuestro LLVM y usar las nuevas optimizaciones agregadas.
Nota separada
La precisión adicional tiene buenos efectos secundarios. Por ejemplo, al leer las solicitudes de grupo del motor Godot, vi que hay una discusión activa sobre si hacer que la precisión de las operaciones de punto flotante sea personalizable en tiempo de compilación (
https://github.com/godotengine/godot/pull/17134 ).
Le pregunté a Juan por qué esto podría ser necesario para alguien, porque creía que las operaciones de punto flotante de 32 bits son suficientes para los juegos.
Juan explicó que, en el caso general, los flotadores funcionan muy bien, pero si se "aleja" del centro, por ejemplo, se mueve a 100 kilómetros del centro del juego, comienza a acumularse un error de cálculo, lo que puede generar interesantes fallas gráficas. Puede usar diferentes estrategias para reducir el impacto de este problema, y una de ellas es trabajar con mayor precisión, por lo que debe pagar por el rendimiento.
Poco después de nuestra conversación en mi feed de Twitter, vi una publicación que demuestra este problema:
http://pharr.org/matt/blog/2018/03/02/rendering-in-camera-space.htmlEl problema se muestra en las imágenes a continuación. Aquí vemos un modelo de automóvil deportivo del paquete pbrt-v3-scenes **
. Tanto la cámara como la escena están cerca del origen, y todo se ve muy bien.**
(Autor de Yasutoshi Mori .)Luego movemos la cámara y la escena 200,000 unidades a xx, yy y zz desde el origen. Se puede ver que el modelo de la máquina se ha fragmentado bastante; Esto se debe únicamente a la falta de precisión en los números de coma flotante.
Si nos movemos aún más 5 × 5 × 5 veces, 1 millón de unidades desde el origen, el modelo comienza a desintegrarse; La máquina se convierte en una aproximación de vóxel extremadamente cruda de sí misma, tanto interesante como aterradora. (Keanu hizo la pregunta: ¿Minecraft es tan cúbico simplemente porque todo está muy alejado del origen?)**
(Pido disculpas a Yasutoshi Mori por lo que hicimos con su hermosa modelo).