Como dobramos a velocidade de trabalhar com o Float em Mono


Meu amigo Aras escreveu recentemente o mesmo ray tracer em diferentes idiomas, incluindo C ++, C # e o compilador Unity Burst. Obviamente, é natural esperar que o C # seja mais lento que o C ++, mas me pareceu interessante que o Mono seja mais lento que o .NET Core.

Seus indicadores publicados eram ruins:

  • C # (.NET Core): Mac 17.5 Mray / s,
  • C # (Unity, Mono): Mac 4.6 Mray / s,
  • C # (Unity, IL2CPP): Mac 17.1 Mray / s

Decidi ver o que estava acontecendo e documentar lugares que poderiam ser melhorados.

Como resultado desse benchmark e do estudo desse problema, encontramos três áreas nas quais a melhoria é possível:

  • Primeiro, você precisa melhorar as configurações Mono padrão, porque os usuários geralmente não definem suas configurações
  • Em segundo lugar, precisamos apresentar ativamente o mundo ao back-end da otimização de código LLVM em Mono
  • Em terceiro lugar, melhoramos o ajuste de alguns parâmetros Mono.

O ponto de referência deste teste foram os resultados do traçador de raios executado na minha máquina e, como tenho hardware diferente, não podemos comparar os números.

Os resultados no meu iMac em casa para Mono e .NET Core foram os seguintes:

Ambiente de trabalhoResultados, MRay / s
.NET Core 2.1.4, dotnet run debug build3.6.
dotnet run -c Release .NET Core 2.1.4 build dotnet run -c Release21,7
Mono de baunilha, mono Maths.exe6.6
Baunilha Mono com LLVM e float3215,5

No processo de estudo desse problema, encontramos alguns problemas, depois de corrigir quais os seguintes resultados foram obtidos:

Ambiente de trabalhoResultados, MRay / s
Mono com LLVM e float3215,5
Mono avançado com LLVM, float32 e inline fixo29,6

O quadro geral:


Apenas aplicando LLVM e float32, você pode aumentar o desempenho do código de ponto flutuante em quase 2,3 vezes. E após o ajuste, que adicionamos ao Mono como resultado dessas experiências, você pode aumentar a produtividade em 4,4 vezes em comparação com o Mono padrão - esses parâmetros nas versões futuras do Mono se tornarão os parâmetros padrão.

Neste artigo, explicarei nossas descobertas.

Flutuação de 32 e 64 bits


Aras usa números de ponto flutuante de 32 bits para a parte principal dos cálculos (digite float em C # ou System.Single em .NET). Em Mono, cometemos um erro há muito tempo - todos os cálculos de ponto flutuante de 32 bits foram executados como 64 bits e os dados ainda estavam armazenados em áreas de 32 bits.

Hoje, minha memória não está tão nítida quanto antes e não me lembro exatamente por que tomamos essa decisão.

Só posso supor que foi influenciado pelas tendências e idéias da época.

Em seguida, uma aura positiva pairava em torno da computação flutuante com maior precisão. Por exemplo, os processadores Intel x87 usavam precisão de 80 bits para cálculos de ponto flutuante, mesmo quando os operandos eram duplos, o que fornecia aos usuários resultados mais precisos.

Naquela época, também era relevante a idéia de que em um dos meus projetos anteriores - planilhas Gnumeric - as funções estatísticas fossem implementadas com mais eficiência do que no Excel. Portanto, muitas comunidades estão bem cientes da ideia de que resultados mais precisos com maior precisão podem ser usados.

Nos estágios iniciais do desenvolvimento Mono, a maioria das operações matemáticas executadas em todas as plataformas podia receber apenas o dobro na entrada. As versões de 32 bits foram adicionadas ao C99, Posix e ISO, mas naquela época elas não estavam amplamente disponíveis para toda a indústria (por exemplo, sinf é a versão flutuante do sin , fabsf é a versão do fabs e assim por diante).

Em suma, o início dos anos 2000 foi um período de otimismo.

Os aplicativos pagavam um preço muito alto pelo aumento do tempo de computação, mas o Mono era usado principalmente para aplicativos Linux de desktop que serviam páginas HTTP e alguns processos de servidor; portanto, a velocidade do ponto flutuante não era o problema que encontramos diariamente. Tornou-se visível apenas em alguns benchmarks científicos e, em 2003, eles raramente foram desenvolvidos no .NET.

Atualmente, jogos, aplicativos 3D, processamento de imagem, VR, AR e aprendizado de máquina tornaram as operações de ponto flutuante um tipo mais comum de dados. O problema não vem sozinho e não há exceções. Float não era mais o tipo de dados amigável usado no código em apenas alguns lugares. Eles se transformaram em uma avalanche, da qual não há onde se esconder. Existem muitos deles e sua propagação não pode ser interrompida.

Sinalizador de espaço de trabalho float32


Portanto, há alguns anos, decidimos adicionar suporte para executar operações de flutuação de 32 bits usando operações de 32 bits, como em todos os outros casos. Chamamos esse recurso de espaço de trabalho de "float32". No Mono, é ativado adicionando a opção --O=float32 no ambiente de trabalho, e nos aplicativos Xamarin esse parâmetro é alterado nas configurações do projeto.

Essa nova bandeira foi bem recebida por nossos usuários de dispositivos móveis, porque basicamente os dispositivos móveis ainda não são muito poderosos e são preferíveis processar dados mais rapidamente do que aumentar a precisão. Recomendamos que os usuários móveis ativem o compilador de otimização LLVM e o sinalizador float32 ao mesmo tempo.

Embora esse sinalizador tenha sido implementado por vários anos, não o tornamos o padrão para evitar surpresas desagradáveis ​​para os usuários. No entanto, começamos a encontrar casos em que surgem surpresas devido ao comportamento padrão de 64 bits. Consulte este relatório de bug enviado pelo usuário do Unity .

Agora usaremos o Mono float32 . O progresso pode ser rastreado aqui: https://github.com/mono/mono/issues/6985 .

Enquanto isso, voltei ao projeto de meu amigo Aras. Ele usou as novas APIs que foram adicionadas ao .NET Core. Embora o .NET Core sempre tenha executado operações de flutuação de 32 bits como flutuadores de 32 bits, a API System.Math ainda realiza conversões de float para double no processo. Por exemplo, se você precisar calcular a função seno para um valor flutuante, a única opção é chamar Math.Sin (double) , e você terá que converter de float para double.

Para corrigir isso, um novo tipo de System.MathF foi adicionado ao .NET Core que contém operações matemáticas com ponto flutuante de precisão única e agora acabamos de mover esse [System.MathF] para Mono .

A transição da flutuação de 64 bits para 32 bits melhora significativamente o desempenho, o que pode ser visto nesta tabela:

Ambiente de trabalho e opçõesMrays / segundo
Mono com System.Math6.6
Mono com System.Math e -O=float328.1
Mono com System.MathF6.5
Mono com System.MathF e -O=float328.2

Ou seja, o uso de float32 nesse teste realmente melhora o desempenho e o MathF tem pouco efeito.

Configuração do LLVM


No processo desta pesquisa, descobrimos que, embora o compilador Fast JIT Mono tenha suporte float32 , não adicionamos esse suporte ao back-end do LLVM. Isso significava que o Mono com LLVM ainda estava realizando conversões dispendiosas de float para double.

Portanto, Zoltan adicionou suporte float32 ao mecanismo de geração de código LLVM.

Então, ele notou que nosso inliner usa as mesmas heurísticas para o Fast JIT que aquelas usadas para o LLVM. Ao trabalhar com o Fast JIT, é necessário encontrar um equilíbrio entre a velocidade do JIT e a velocidade de execução; portanto, limitamos a quantidade de código incorporado para reduzir a quantidade de trabalho do mecanismo JIT.

Mas se você decidir usar o LLVM no Mono, esforçar-se-á pelo código o mais rápido possível, por isso alteramos as configurações de acordo. Hoje, esse parâmetro pode ser alterado usando a MONO_INLINELIMIT ambiente MONO_INLINELIMIT , mas na verdade ele precisa ser gravado nos valores padrão.

Aqui estão os resultados com as configurações LLVM modificadas:

Ambiente de trabalho e opçõesMrays / segundos
Mono com System.Math --llvm -O=float3216,0
Mono com System.Math --llvm -O=float32 , heurísticas constantes29,1
Mono com System.MathF --llvm -O=float32 , heurísticas constantes29,6

Próximas etapas


Pouco esforço foi necessário para fazer todas essas melhorias. Essas mudanças foram lideradas por discussões periódicas no Slack. Até consegui fazer algumas horas uma noite para portar System.MathF para Mono.

O código do rastreador de raios Aras tornou-se um assunto ideal para estudo porque era auto-suficiente, era uma aplicação real e não uma referência sintética. Queremos encontrar outro software semelhante que possa ser usado para estudar o código binário que geramos e garantir que passemos ao LLVM os melhores dados para a execução ideal de seu trabalho.

Também estamos considerando atualizar nosso LLVM e usar as novas otimizações adicionadas.

Nota separada


Precisão extra tem bons efeitos colaterais. Por exemplo, lendo as solicitações de pool do mecanismo Godot, vi que há uma discussão ativa sobre se a precisão das operações de ponto flutuante pode ser personalizada em tempo de compilação ( https://github.com/godotengine/godot/pull/17134 ).

Perguntei a Juan por que isso pode ser necessário para alguém, porque acreditava que operações de ponto flutuante de 32 bits são suficientes para jogos.

Juan explicou que, no caso geral, os carros alegóricos funcionam muito bem, mas se você "se afastar" do centro, digamos, se move 100 quilômetros do centro do jogo, um erro de cálculo começa a se acumular, o que pode levar a falhas gráficas interessantes. Você pode usar estratégias diferentes para reduzir o impacto desse problema, e uma delas é trabalhar com maior precisão, pela qual você deve pagar pelo desempenho.

Logo após nossa conversa, no meu feed do Twitter, vi uma postagem demonstrando esse problema: http://pharr.org/matt/blog/2018/03/02/rendering-in-camera-space.html

O problema é mostrado nas imagens abaixo. Aqui vemos um modelo de carro esportivo do pacote pbrt-v3-scenes ** . A câmera e a cena estão próximas da origem e tudo parece ótimo.


** (Autor de Yasutoshi Mori .)

Em seguida, movemos a câmera e a cena 200.000 unidades em xx, yy e zz a partir da origem. Pode-se ver que o modelo da máquina se tornou bastante fragmentado; isso se deve unicamente à falta de precisão nos números de ponto flutuante.


Se avançarmos ainda mais 5 × 5 × 5 vezes, 1 milhão de unidades a partir da origem, o modelo começará a se desintegrar; a máquina se transforma em uma aproximação voxel extremamente grosseira de si mesma, interessante e aterrorizante. (Keanu fez a pergunta: O Minecraft é tão cúbico simplesmente porque tudo é renderizado muito longe da origem?)


** (Peço desculpas a Yasutoshi Mori pelo que fizemos com sua bela modelo.)

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


All Articles