Aujourd'hui, nous allons examiner deux autres endroits où pbrt passe beaucoup de temps à analyser des scènes du dessin animé Disney
"Moana" . Voyons voir s'il sera possible d'améliorer la productivité ici. Cela se termine par ce qu'il est prudent de faire dans pbrt-v3. Dans un autre article, je traiterai de la mesure dans laquelle nous pouvons aller si nous abandonnons l'interdiction des changements. Dans ce cas, le code source sera trop différent du système décrit dans le livre
Physically Based Rendering .
Optimisation de l'analyseur
Après les améliorations de performances introduites dans l'
article précédent , la proportion de temps passé dans l'analyseur pbrt, et si importante dès le début, a naturellement augmenté encore plus. Actuellement, l'analyseur au démarrage est utilisé la plupart du temps.
J'ai finalement rassemblé mes forces et
mis en œuvre un tokenizer et un analyseur écrits manuellement pour les scènes pbrt.
Le format des fichiers de scène pbrt est assez simple à
analyser : si vous ne prenez pas en compte les lignes entre guillemets, les jetons sont séparés par des espaces, et la grammaire est très simple (il n'est jamais nécessaire de regarder plus d'un jeton vers l'avant), mais votre propre analyseur est toujours un millier de lignes de code dont vous avez besoin écrire et déboguer. Cela m'a aidé à pouvoir le tester sur de nombreuses scènes; après avoir corrigé des problèmes évidents, j'ai continué à travailler jusqu'à ce que je parvienne à rendre exactement les mêmes images qu'auparavant: il ne devrait pas y avoir de différences de pixels en raison du remplacement de l'analyseur. À ce stade, j'étais absolument sûr que tout avait été fait correctement.
J'ai essayé de rendre la nouvelle version aussi efficace que possible, en soumettant les fichiers d'entrée à
mmap()
autant que possible et en utilisant la nouvelle implémentation de
std::string_view
de C ++ 17 pour minimiser la création de copies de chaînes à partir du contenu du fichier. De plus, comme
strtod()
pris beaucoup de temps dans les traces précédentes, j'ai écrit
parseNumber()
avec un soin particulier: les entiers à un chiffre et les entiers réguliers sont traités séparément, et dans le cas standard lorsque pbrt est compilé pour utiliser des flottants 32 bits , a utilisé
strtof()
au lieu de
strtod()
1 .
Dans le processus de création d'une implémentation du nouvel analyseur, j'avais un peu peur que l'ancien analyseur soit plus rapide: au final, flex et bison ont été développés et optimisés depuis de nombreuses années. Je ne pouvais pas savoir à l'avance si tout le temps que j'écrirais une nouvelle version serait gaspillé jusqu'à ce que je la termine et la fasse fonctionner correctement.
À ma grande joie, notre propre analyseur s'est avéré être une énorme victoire: la généralisation du flex et du bison a tellement réduit les performances que la nouvelle version les a facilement dépassées. Grâce au nouvel analyseur, le temps de lancement a diminué à 13 min 21 s, c'est-à-dire qu'il a accéléré encore 1,5 fois! Un bonus supplémentaire était qu'il était désormais possible de supprimer tout le support flex et bison du système de construction pbrt. Cela a toujours été un casse-tête, en particulier sous Windows, où la plupart des gens ne l'ont pas installé par défaut.
Gestion de l'état des graphiques
Après avoir considérablement accéléré l'analyseur, un nouveau détail ennuyeux est apparu: à ce stade, environ 10% du temps de configuration a été consacré aux fonctions
pbrtAttributeBegin()
et
pbrtAttributeEnd()
, et la plupart de ce temps a été alloué et libéré de la mémoire dynamique. Lors de la première exécution, qui a duré 35 minutes, ces fonctions ne prenaient qu'environ 3% du temps d'exécution, de sorte qu'elles pouvaient être ignorées. Mais avec l'optimisation, c'est toujours comme ça: lorsque vous commencez à vous débarrasser des gros problèmes, les petits deviennent plus importants.
La description de la scène pbrt est basée sur l'état hiérarchique du graphique, qui indique la transformation actuelle, le matériau actuel, etc. Vous pouvez y créer des instantanés de l'état actuel (
pbrtAttributeBegin()
), y apporter des modifications avant d'ajouter une nouvelle géométrie à la scène, puis revenir à l'état d'origine (
pbrtAttributeEnd()
).
L'état graphique est stocké dans une structure avec un nom inattendu ...
GraphicsState
. Pour stocker des copies des objets
GraphicsState
dans la pile des états graphiques enregistrés,
std::vector
. En regardant les membres
GraphicsState
, nous pouvons supposer la source des problèmes - trois
std::map
, des noms aux instances de textures et de matériaux:
struct GraphicsState {
En examinant ces fichiers de scène, j'ai constaté que la plupart des cas d'enregistrement et de restauration de l'état graphique sont effectués dans ces lignes:
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 d'autres termes, il met à jour la transformation actuelle et instancie l'objet; aucune modification n'est apportée au contenu de ces
std::map
. En créer une copie complète - allouer des nœuds d'arbre rouge-noir, augmenter le nombre de références pour les pointeurs communs, allouer de l'espace et copier des chaînes - est presque toujours une perte de temps. Tout cela est libéré lors de la restauration de l'état précédent des graphiques.
J'ai remplacé chacune de ces cartes par le pointeur
std::shared_ptr
pour mapper et mis en œuvre l'approche de copie sur écriture, dans laquelle la copie à l'intérieur du bloc de début / fin d'un attribut ne se produit que lorsque son contenu doit être modifié.
Le changement n'a pas été particulièrement difficile, mais il a réduit le temps de lancement de plus d'une minute, ce qui nous a donné 12 min 20 s de traitement avant le début du rendu - encore une fois une accélération de 1,08 fois.
Et le temps de rendu?
Un lecteur attentif remarquera que jusqu'à présent je n'ai rien dit sur le temps de rendu. À ma grande surprise, cela s'est avéré tout à fait tolérable même hors de la boîte: pbrt peut restituer des images de scènes de qualité cinématographique avec plusieurs centaines d'échantillons par pixel sur douze cœurs de processeur pendant une période de deux à trois heures. Par exemple, cette image, l'une des plus lentes, rendue en 2 heures 51 minutes 36 secondes:
Dunes de Moana rendues par pbrt-v3 avec une résolution de 2048x858 à 256 échantillons par pixel. Le temps de rendu total sur une instance de Google Compute Engine avec 12 cœurs / 24 threads avec une fréquence de 2 GHz et la dernière version de pbrt-v3 était de 2 heures 51 minutes 36 secondes.À mon avis, cela semble être un indicateur étonnamment raisonnable. Je suis sûr que des améliorations sont encore possibles, et une étude attentive des endroits où la plupart du temps est passé révèlera beaucoup de choses «intéressantes», mais jusqu'à présent, il n'y a pas de raisons particulières pour cela.
Lors du profilage, il s'est avéré qu'environ 60% du temps de rendu a été passé à l'intersection des rayons avec des objets (la plupart des opérations ont été effectuées en contournant BVH), et 25% a été consacré à la recherche de textures ptex. Ces ratios sont similaires aux indicateurs de scènes plus simples, donc à première vue il n'y a rien de bien évident ici. (Cependant, je suis sûr qu'Embree sera capable de retracer ces rayons en un peu moins de temps.)
Malheureusement, l'évolutivité parallèle n'est pas si bonne. Je constate généralement que 1400% des ressources CPU sont consacrées au rendu, par rapport à l'idéal de 2400% (sur 24 CPU virtuels dans Google Compute Engine). Il semble que le problème soit lié aux conflits lors des verrous dans ptex, mais je ne l'ai pas encore étudié plus en détail. Il est très probable que pbrt-v3 ne calcule pas la différence de rayons pour les rayons indirects dans le traceur de rayons; à leur tour, ces faisceaux ont toujours accès au niveau MIP le plus détaillé des textures, ce qui n'est pas très utile pour la mise en cache des textures.
Conclusion (pour pbrt-v3)
Après avoir corrigé la gestion de l'état des graphiques, je suis tombé sur une limite, après quoi de nouveaux progrès sans apporter de modifications importantes au système sont devenus évidents; tout le reste a pris beaucoup de temps et n'avait pas grand-chose à voir avec l'optimisation. Par conséquent, je m'attarderai là-dessus, au moins en ce qui concerne pbrt-v3.
En général, la progression était sérieuse: le temps de lancement avant rendu est passé de 35 minutes à 12 minutes 20 secondes, c'est-à-dire que l'accélération totale était de 2,83 fois. De plus, grâce à un travail intelligent avec le cache de conversion, l'utilisation de la mémoire est passée de 80 Go à 69 Go. Toutes ces modifications sont disponibles maintenant si vous vous synchronisez avec la dernière version de pbrt-v3 (ou si vous l'avez fait au cours des derniers mois). Et nous arrivons à comprendre à quel point la mémoire
Primitive
est encombrante pour cette scène; nous avons compris comment économiser encore 18 Go de mémoire, mais ne l'avons pas implémenté dans pbrt-v3.
Voici à quoi ces 12 min 20 s sont consacrées après toutes nos optimisations:
Fonction / fonctionnement | Pourcentage de durée d'exécution |
---|
Build BVH | 34% |
Analyse (sauf strtof() ) | 21% |
strtof() | 20% |
Cache de conversion | 7% |
Lecture de fichiers PLY | 6% |
Allocation dynamique de mémoire | 5% |
Inversion de conversion | 2% |
Gestion de l'état des graphiques | 2% |
Autre | 3% |
À l'avenir, la meilleure option pour améliorer les performances sera un multithreading encore plus important de la phase de lancement: presque tout pendant l'analyse de la scène est monothread; notre premier objectif le plus naturel est de construire un BVH. Il sera également intéressant d'analyser des choses telles que la lecture de fichiers PLY et la génération de BVH pour des instances individuelles d'objets et leur exécution asynchrone en arrière-plan, tandis que l'analyse sera effectuée dans le thread principal.
À un moment donné, je verrai s'il existe des implémentations plus rapides de
strtof()
; pbrt utilise uniquement ce que le système fournit. Cependant, vous devez être prudent avec le choix des remplacements qui ne sont pas testés de manière très approfondie: l'analyse des valeurs flottantes est l'un de ces aspects dont le programmeur doit être complètement sûr.
Il semble également intéressant de réduire davantage la charge de l'analyseur: nous avons encore 17 Go de fichiers d'entrée de texte pour l'analyse. Nous pouvons ajouter un support d'encodage binaire pour les fichiers d'entrée pbrt (peut-être similaire à
l'approche RenderMan ), mais j'ai des sentiments mitigés sur cette idée; La possibilité d'ouvrir et de modifier des fichiers de description de scène dans un éditeur de texte est très utile, et je crains que le codage binaire ne perturbe parfois les élèves utilisant pbrt dans le processus d'apprentissage. C'est l'un de ces cas où la bonne solution pour pbrt peut différer des solutions pour un rendu commercial d'un niveau de production.
C'était très intéressant de garder une trace de toutes ces optimisations et de mieux comprendre les différentes solutions. Il s'est avéré que pbrt a des hypothèses inattendues qui interfèrent avec la scène de ce niveau de complexité. Tout cela est un excellent exemple de l'importance pour une large communauté de chercheurs en rendu d'avoir accès à des scènes de production réelles avec un haut degré de complexité; Je remercie encore une fois Disney pour le temps passé à traiter cette scène et à la mettre dans le domaine public.
Dans le
prochain article , nous examinerons les aspects qui peuvent encore améliorer les performances si nous permettons à pbrt d'apporter des modifications plus radicales.
Remarque
- Sur le système Linux sur
strtof()
je strtof()
, strtof()
pas plus rapide que strtod()
. Il est à noter que sous OS X, strtod()
environ deux fois plus rapide, ce qui est complètement illogique. Pour des raisons pratiques, j'ai continué à utiliser strtof()
.