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 {
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ção | Porcentagem de tempo de execução |
---|
Build BVH | 34% |
Análise (exceto strtof() ) | 21% |
strtof() | 20% |
Cache de conversão | 7% |
Lendo arquivos PLY | 6% |
Alocação dinâmica de memória | 5% |
Inversão de conversão | 2% |
Gerenciamento de status gráfico | 2% |
Outros | 3% |
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
- 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()
.