Otimização da renderização de uma cena do desenho animado da Disney "Moana". Parte 3

imagem

Hoje, veremos mais dois lugares em que o pbrt passa muito tempo analisando cenas do desenho animado da Disney "Moana" . Vamos ver se será possível melhorar a produtividade aqui. Isso termina com o que é prudente fazer no pbrt-v3. Em outro post, abordarei até onde podemos chegar se abandonarmos a proibição de alterações. Nesse caso, o código fonte será muito diferente do sistema descrito no livro Renderização fisicamente baseada .

Otimização do analisador


Após as melhorias de desempenho introduzidas no artigo anterior , a proporção de tempo gasto no analisador pbrt e tão significativa desde o início aumentou naturalmente ainda mais. Atualmente, o analisador na inicialização é gasto a maior parte do tempo.

Finalmente reuni forças e implementei um tokenizador e analisador escritos manualmente para cenas pbrt. O formato dos arquivos de cena pbrt é bastante simples de analisar : se você não levar em conta as linhas citadas, os tokens serão separados por espaços e a gramática é muito direta (nunca haverá a necessidade de olhar mais de um token), mas seu próprio analisador ainda precisará de mil linhas de código. escrever e depurar. Ajudou-me que pudesse ser testado em muitas cenas; depois de corrigir falhas óbvias, continuei trabalhando até conseguir renderizar exatamente as mesmas imagens de antes: não deve haver diferenças nos pixels devido à substituição do analisador. Nesta fase, eu tinha certeza absoluta de que tudo foi feito corretamente.

Tentei tornar a nova versão o mais eficiente possível, sujeitando os arquivos de entrada a mmap() o máximo possível e usando a nova implementação de std::string_view do C ++ 17 para minimizar a criação de cópias de strings a partir do conteúdo do arquivo. Além disso, como strtod() demorou muito tempo em rastreamentos anteriores, escrevi parseNumber() com cuidado especial: números inteiros de um dígito e números regulares regulares são processados ​​separadamente e, no caso padrão, quando o pbrt é compilado para usar flutuadores de 32 bits , usou strtof() vez de strtod() 1 .

No processo de criação de uma implementação do novo analisador, fiquei com um pouco de medo de que o analisador antigo fosse mais rápido: no final, flex e bison foram desenvolvidos e otimizados por muitos anos. Não pude descobrir com antecedência se o tempo todo escrevendo uma nova versão seria desperdiçado até que eu a concluísse e a fizesse funcionar corretamente.

Para minha alegria, nosso próprio analisador acabou sendo uma grande vitória: a generalização de flex e bison reduziu tanto o desempenho que a nova versão os ultrapassou facilmente. Graças ao novo analisador, o tempo de inicialização diminuiu para 13 min 21 s, ou seja, acelerou outras 1,5 vezes! Um bônus adicional era que agora era possível remover todo o suporte flex e bison do sistema pbrt build. Sempre foi uma dor de cabeça, especialmente no Windows, onde a maioria das pessoas não a instala por padrão.

Gerenciamento de status gráfico


Após acelerar significativamente o analisador, surgiu um novo detalhe irritante: nesse estágio, aproximadamente 10% do tempo de configuração foi gasto nas funções pbrtAttributeBegin() e pbrtAttributeEnd() , e a maior parte desse tempo foi alocada e liberada memória dinâmica. Durante a primeira execução, que levou 35 minutos, essas funções levaram apenas cerca de 3% do tempo de execução, para que pudessem ser ignoradas. Mas com a otimização, é sempre assim: quando você começa a se livrar de grandes problemas, os pequenos se tornam mais importantes.

A descrição da cena pbrt é baseada no estado hierárquico do gráfico, que indica a transformação atual, o material atual e assim por diante. Nele, é possível fazer instantâneos do estado atual ( pbrtAttributeBegin() ), fazer alterações antes de adicionar uma nova geometria à cena e retornar ao estado original ( pbrtAttributeEnd() ).

O estado dos gráficos é armazenado em uma estrutura com um nome inesperado ... GraphicsState . Para armazenar cópias de objetos GraphicsState na pilha de estados gráficos salvos, std::vector . Observando os membros da GraphicsState , podemos assumir a origem dos problemas - três std::map , de nomes a instâncias de texturas e materiais:

 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; }; 

Examinando esses arquivos de cena, descobri que a maioria dos casos de salvamento e restauração de estado de gráficos é realizada nestas linhas:

 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 

Em outras palavras, ele atualiza a transformação atual e instancia o objeto; nenhuma alteração é feita no conteúdo desses std::map . Criar uma cópia completa deles - alocar nós de árvore vermelho-preto, aumentar as contagens de referência para ponteiros comuns, alocar espaço e copiar seqüências de caracteres - é quase sempre uma perda de tempo. Tudo isso é liberado ao restaurar o estado anterior dos gráficos.

Substituí cada um desses mapas pelo ponteiro std::shared_ptr para mapear e implementei a abordagem de copiar na gravação, na qual a cópia dentro do bloco de início / fim de um atributo ocorre apenas quando seu conteúdo precisa ser alterado. A mudança não foi particularmente difícil, mas reduziu o tempo de inicialização em mais de um minuto, o que nos deu 12 min 20 s de processamento antes do início da renderização - novamente uma aceleração de 1,08 vezes.

E quanto ao tempo de renderização?


Um leitor atento perceberá que até agora eu não disse nada sobre o tempo de renderização. Para minha surpresa, acabou sendo bastante tolerável, fora da caixa: o pbrt pode renderizar imagens de cenas de qualidade cinematográfica com várias centenas de amostras por pixel em doze núcleos de processador por um período de duas a três horas. Por exemplo, esta imagem, uma das mais lentas, renderizada em 2 horas 51 minutos e 36 segundos:


Dunas de Moana renderizadas por pbrt-v3 com uma resolução de 2048x858 a 256 amostras por pixel. O tempo total de renderização em uma instância do Google Compute Engine com 12 núcleos / 24 threads com uma frequência de 2 GHz e a versão mais recente do pbrt-v3 foi de 2 horas 51 minutos 36 segundos.

Na minha opinião, isso parece um indicador surpreendentemente razoável. Estou certo de que as melhorias ainda são possíveis, e um estudo cuidadoso dos lugares em que a maior parte do tempo é gasto revelará muitas coisas "interessantes", mas até agora não há motivos especiais para elas.

Ao criar um perfil, verificou-se que aproximadamente 60% do tempo de renderização era gasto na interseção de raios com objetos (a maioria das operações era realizada ignorando o BVH) e 25% era gasto na busca de texturas de ptex. Essas proporções são semelhantes aos indicadores de cenas mais simples, portanto, à primeira vista, não há nada obviamente problemático aqui. (No entanto, tenho certeza de que a Embree poderá rastrear esses raios em um pouco menos de tempo.)

Infelizmente, a escalabilidade paralela não é tão boa. Normalmente, vejo que 1400% dos recursos da CPU são gastos na renderização, em comparação com o ideal de 2400% (em 24 CPUs virtuais no Google Compute Engine). Parece que o problema está relacionado a conflitos durante bloqueios no ptex, mas ainda não o investiguei com mais detalhes. É muito provável que o pbrt-v3 não calcule a diferença de raios para raios indiretos no traçador de raios; por sua vez, essas vigas sempre obtêm acesso ao nível MIP mais detalhado de texturas, o que não é muito útil para o cache de textura.

Conclusão (para pbrt-v3)


Tendo corrigido o gerenciamento do estado dos gráficos, cheguei a um limite, após o qual o progresso adicional sem fazer alterações significativas no sistema se tornou óbvio; todo o resto levou muito tempo e pouco teve a ver com otimização. Portanto, vou me debruçar sobre isso, pelo menos no que diz respeito ao pbrt-v3.

Em geral, o progresso foi sério: o tempo de inicialização antes da renderização diminuiu de 35 minutos para 12 minutos e 20 segundos, ou seja, a aceleração total foi de 2,83 vezes. Além disso, graças ao trabalho inteligente com o cache de conversão, o uso da memória diminuiu de 80 GB para 69 GB. Todas essas alterações estão disponíveis agora se você estiver sincronizando com a versão mais recente do pbrt-v3 (ou se tiver feito isso nos últimos meses). E chegamos a entender como a memória do Primitive é lixo para essa cena; descobrimos como economizar outros 18 GB de memória, mas não o implementamos no pbrt-v3.

Aqui está o que esses 12 min 20 s são gastos após todas as nossas otimizações:

Função / OperaçãoPorcentagem de tempo de execução
Build BVH34%
Análise (exceto strtof() )21%
strtof()20%
Cache de conversão7%
Lendo arquivos PLY6%
Alocação dinâmica de memória5%
Inversão de conversão2%
Gerenciamento de status gráfico2%
Outros3%

No futuro, a melhor opção para melhorar o desempenho será o multithreading ainda maior do estágio de lançamento: quase tudo durante a análise da cena é de thread único; nosso primeiro objetivo mais natural é construir um BVH. Também será interessante analisar coisas como ler arquivos PLY e gerar BVH para instâncias individuais de objetos e executá-los de forma assíncrona em segundo plano, enquanto a análise será realizada no encadeamento principal.

Em algum momento, verei se há implementações mais rápidas de strtof() ; O pbrt usa apenas o que o sistema fornece. No entanto, você deve ter cuidado com a escolha de substituições que não são exaustivamente testadas: a análise de valores flutuantes é um daqueles aspectos dos quais o programador deve ter certeza absoluta.

Também parece atraente para reduzir ainda mais a carga no analisador: ainda temos 17 GB de arquivos de entrada de texto para análise. Podemos adicionar suporte à codificação binária para arquivos de entrada pbrt (possivelmente semelhante à abordagem RenderMan ), mas tenho sentimentos contraditórios sobre essa idéia; a capacidade de abrir e modificar arquivos de descrição de cenas em um editor de texto é bastante útil, e estou preocupado que algumas vezes a codificação binária confunda os alunos que usam o pbrt no processo de aprendizado. Esse é um daqueles casos em que a solução certa para pbrt pode diferir das soluções para uma renderização comercial de um nível de produção.

Foi muito interessante acompanhar todas essas otimizações e entender melhor várias soluções. Descobriu-se que o pbrt tem suposições inesperadas que interferem na cena desse nível de complexidade. Tudo isso é um ótimo exemplo de quão importante é para uma ampla comunidade de renderizar pesquisadores ter acesso a cenas reais de produção com um alto grau de complexidade; Mais uma vez, agradeço à Disney pelo tempo gasto em processar essa cena e colocá-la em domínio público.

No próximo artigo , examinaremos aspectos que podem melhorar ainda mais o desempenho se permitirmos que o pbrt faça alterações mais radicais.

Nota


  1. No sistema Linux em que eu estava testando, strtof() não strtof() mais rápido que strtod() . Vale ressaltar que no OS X strtod() cerca de duas vezes mais rápido, o que é completamente ilógico. Por razões práticas, continuei usando strtof() .

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


All Articles