Optimización de la representación de una escena de la caricatura de Disney "Moana". Parte 3

imagen

Hoy veremos dos lugares más donde pbrt pasa mucho tiempo analizando escenas de la caricatura de Disney "Moana" . Veamos si será posible mejorar la productividad aquí. Esto concluye con lo que es prudente hacer en pbrt-v3. En otra publicación, me ocuparé de hasta dónde podemos llegar si abandonamos la prohibición de los cambios. En este caso, el código fuente será demasiado diferente del sistema descrito en el libro Representación basada en la física .

Optimización del analizador


Después de las mejoras de rendimiento introducidas en el artículo anterior , la proporción de tiempo dedicado al analizador pbrt, y tan importante desde el principio, naturalmente aumentó aún más. Actualmente, el analizador al inicio se pasa la mayor parte del tiempo.

Finalmente reuní mi fuerza e implementé un tokenizador y analizador escrito manualmente para escenas pbrt. El formato de los archivos de escena pbrt es bastante simple de analizar : si no tiene en cuenta las líneas citadas, los tokens están separados por espacios, y la gramática es muy sencilla (nunca es necesario mirar hacia adelante más de un token), pero su propio analizador todavía tiene mil líneas de código que necesita escribir y depurar Me ayudó a poder probarlo en muchas escenas; Después de solucionar problemas técnicos obvios, continué trabajando hasta que logré procesar exactamente las mismas imágenes que antes: no debería haber ninguna diferencia en los píxeles debido a la sustitución del analizador. En esta etapa, estaba absolutamente seguro de que todo se hizo correctamente.

Traté de hacer que la nueva versión sea lo más eficiente posible, sometiendo los archivos de entrada a mmap() tanto como sea posible y usando la nueva implementación de std::string_view de C ++ 17 para minimizar la creación de copias de cadenas a partir del contenido del archivo. Además, dado que strtod() tomó mucho tiempo en las trazas anteriores, escribí parseNumber() con especial cuidado: los enteros de un solo dígito y los enteros regulares se procesan por separado, y en el caso estándar cuando se compila pbrt para usar flotantes de 32 bits , usó strtof() lugar de strtod() 1 .

En el proceso de crear una implementación del nuevo analizador, tenía un poco de miedo de que el analizador anterior fuera más rápido: al final, flex y bison se han desarrollado y optimizado durante muchos años. No pude averiguar de antemano si se desperdiciaría todo el tiempo escribiendo una nueva versión hasta que la completara y la hiciera funcionar correctamente.

Para mi alegría, nuestro propio analizador resultó ser una gran victoria: la generalización de flex y bison redujo tanto el rendimiento que la nueva versión los superó fácilmente. Gracias al nuevo analizador, el tiempo de lanzamiento disminuyó a 13 min 21 s, es decir, ¡se aceleró otras 1,5 veces! Una ventaja adicional era que ahora era posible eliminar todo el soporte de flex y bison del sistema de compilación pbrt. Siempre ha sido un dolor de cabeza, especialmente en Windows, donde la mayoría de las personas no lo tienen instalado de forma predeterminada.

Gestión de estado de gráficos


Después de acelerar significativamente el analizador, surgió un nuevo detalle molesto: en esta etapa, aproximadamente el 10% del tiempo de configuración se gastó en las funciones pbrtAttributeBegin() y pbrtAttributeEnd() , y la mayor parte de este tiempo se asignó y liberó memoria dinámica. Durante la primera ejecución, que tomó 35 minutos, estas funciones tomaron solo alrededor del 3% del tiempo de ejecución, por lo que podrían ignorarse. Pero con la optimización siempre es así: cuando comienzas a deshacerte de los grandes problemas, los pequeños se vuelven más importantes.

La descripción de la escena pbrt se basa en el estado jerárquico del gráfico, que indica la transformación actual, el material actual, etc. En él, puede tomar instantáneas del estado actual ( pbrtAttributeBegin() ), realizar cambios antes de agregar una nueva geometría a la escena y luego volver al estado original ( pbrtAttributeEnd() ).

El estado de los gráficos se almacena en una estructura con un nombre inesperado ... GraphicsState . Para almacenar copias de objetos GraphicsState en la pila de estados gráficos guardados, std::vector . Mirando a los miembros de GraphicsState , podemos asumir la fuente de los problemas: tres std::map , desde nombres hasta instancias de texturas y materiales:

 struct GraphicsState { // ... std::map<std::string, std::shared_ptr<Texture<Float>>> floatTextures; std::map<std::string, std::shared_ptr<Texture<Spectrum>>> spectrumTextures; std::map<std::string, std::shared_ptr<MaterialInstance>> namedMaterials; }; 

Al examinar estos archivos de escena, descubrí que la mayoría de los casos de guardar y restaurar el estado de los gráficos se realizan en estas líneas:

 AttributeBegin ConcatTransform [0.981262 0.133695 -0.138749 0.000000 -0.067901 0.913846 0.400343 0.000000 0.180319 -0.383420 0.905800 0.000000 11.095301 18.852249 9.481399 1.000000] ObjectInstance "archivebaycedar0001_mod" AttributeEnd 

En otras palabras, actualiza la transformación actual e instancia el objeto; no se realizan cambios en el contenido de estos std::map . Crear una copia completa de ellos, asignar nodos de árbol rojo-negro, aumentar los recuentos de referencias para punteros comunes, asignar espacio y copiar cadenas, es casi siempre una pérdida de tiempo. Todo esto se libera al restaurar el estado anterior de los gráficos.

std::shared_ptr cada uno de estos mapas con el puntero std::shared_ptr para mapear e implementé el enfoque de copia en escritura, en el que la copia dentro del bloque de inicio / fin de un atributo ocurre solo cuando es necesario cambiar su contenido. El cambio no fue particularmente difícil, pero redujo el tiempo de lanzamiento en más de un minuto, lo que nos dio 12 min 20 s de procesamiento antes del inicio del renderizado, nuevamente una aceleración de 1.08 veces.

¿Qué pasa con el tiempo de renderizado?


Un lector atento notará que hasta ahora no he dicho nada sobre el tiempo de representación. Para mi sorpresa, resultó ser bastante tolerable incluso fuera de la caja: pbrt puede renderizar imágenes de escenas de calidad cinematográfica con varios cientos de muestras por píxel en doce núcleos de procesador durante un período de dos a tres horas. Por ejemplo, esta imagen, una de las más lentas, renderizada en 2 horas 51 minutos 36 segundos:


Dunas de Moana renderizadas por pbrt-v3 con una resolución de 2048x858 a 256 muestras por píxel. El tiempo total de representación en una instancia de Google Compute Engine con 12 núcleos / 24 subprocesos con una frecuencia de 2 GHz y la última versión de pbrt-v3 fue de 2 horas 51 minutos 36 segundos.

En mi opinión, esto parece un indicador sorprendentemente razonable. Estoy seguro de que las mejoras aún son posibles, y un estudio cuidadoso de los lugares en los que se pasa la mayor parte del tiempo revelará muchas cosas "interesantes", pero hasta ahora no hay razones especiales para ello.

Al perfilar, resultó que aproximadamente el 60% del tiempo de renderizado se gastó en la intersección de los rayos con los objetos (la mayoría de las operaciones se realizaron sin pasar por BVH), y el 25% se gastó buscando texturas ptex. Estas proporciones son similares a los indicadores de escenas más simples, por lo que a primera vista no hay nada obviamente problemático aquí. (Sin embargo, estoy seguro de que Embree podrá rastrear estos rayos en un poco menos de tiempo).

Desafortunadamente, la escalabilidad paralela no es tan buena. Por lo general, veo que el 1400% de los recursos de la CPU se gastan en renderizado, en comparación con el ideal del 2400% (en 24 CPU virtuales en Google Compute Engine). Parece que el problema está relacionado con conflictos durante bloqueos en ptex, pero aún no lo he investigado con más detalle. Es muy probable que pbrt-v3 no calcule la diferencia de rayos para los rayos indirectos en el rastreador de rayos; a su vez, tales haces siempre obtienen acceso al nivel de texturas MIP más detallado, lo que no es muy útil para el almacenamiento en caché de texturas.

Conclusión (para pbrt-v3)


Habiendo corregido la gestión del estado de los gráficos, me encontré con un límite, después del cual no se hicieron evidentes progresos adicionales sin realizar cambios significativos en el sistema; todo el resto tomó mucho tiempo y tuvo poco que ver con la optimización. Por lo tanto, me detendré en esto, al menos con respecto a pbrt-v3.

En general, el progreso fue serio: el tiempo de lanzamiento antes del renderizado disminuyó de 35 minutos a 12 minutos y 20 segundos, es decir, la aceleración total fue de 2,83 veces. Además, gracias al trabajo inteligente con el caché de conversión, el uso de memoria ha disminuido de 80 GB a 69 GB. Todos estos cambios están disponibles ahora si está sincronizando con la última versión de pbrt-v3 (o si lo ha hecho en los últimos meses). Y entendemos cuán basura es la memoria Primitive para esta escena; descubrimos cómo guardar otros 18 GB de memoria, pero no lo implementamos en pbrt-v3.

Esto es en lo que se gastan estos 12 min 20 s después de todas nuestras optimizaciones:

Función / OperaciónPorcentaje de tiempo de ejecución
Build BVH34%
Análisis (excepto strtof() )21%
strtof()20%
Caché de conversión7%
Leer archivos PLY6%
Asignación de memoria dinámica5%
Inversión de conversión2%
Gestión de estado de gráficos2%
Otros3%

En el futuro, la mejor opción para mejorar el rendimiento será una multiproceso aún mayor de la etapa de lanzamiento: casi todo durante el análisis de la escena es de un solo subproceso; Nuestro primer objetivo más natural es construir un BVH. También será interesante analizar cosas como leer archivos PLY y generar BVH para instancias individuales de objetos y ejecutarlos de forma asíncrona en segundo plano, mientras que el análisis se realizará en el hilo principal.

En algún momento, veré si hay implementaciones más rápidas de strtof() ; pbrt usa solo lo que proporciona el sistema. Sin embargo, debe tener cuidado con la elección de los reemplazos que no se prueban muy a fondo: analizar los valores flotantes es uno de esos aspectos de los que el programador debe estar completamente seguro.

También parece atractivo reducir aún más la carga en el analizador: todavía tenemos 17 GB de archivos de entrada de texto para analizar. Podemos agregar soporte de codificación binaria para archivos de entrada pbrt (posiblemente similar al enfoque RenderMan ), pero tengo sentimientos encontrados acerca de esta idea; La capacidad de abrir y modificar archivos de descripción de escena en un editor de texto es bastante útil, y me preocupa que a veces la codificación binaria confunda a los estudiantes que usan pbrt en el proceso de aprendizaje. Este es uno de esos casos en los que la solución correcta para pbrt puede diferir de las soluciones para una representación comercial de un nivel de producción.

Fue muy interesante hacer un seguimiento de todas estas optimizaciones y comprender mejor varias soluciones. Resultó que pbrt tiene suposiciones inesperadas que interfieren con la escena de este nivel de complejidad. Todo esto es un gran ejemplo de lo importante que es para una amplia comunidad de investigadores de acceso tener acceso a escenas de producción reales con un alto grado de complejidad; Nuevamente le agradezco a Disney por el tiempo dedicado a procesar esta escena y ponerla en el dominio público.

En el próximo artículo , analizaremos aspectos que pueden mejorar aún más el rendimiento si permitimos que pbrt realice cambios más radicales.

Nota


  1. En el sistema Linux en el que estaba probando, strtof() no strtof() más rápido que strtod() . Es de destacar que en OS X strtod() aproximadamente dos veces más rápido, lo cual es completamente ilógico. Por razones prácticas, seguí usando strtof() .

Source: https://habr.com/ru/post/es417913/


All Articles