Bom, acredito que nunca tenha postado algo específico do meu trabalho.
Um dos motivos para isso seria que parte das coisas que faço são muito sensíveis para compartilhar.
Mas esse final de semana comecei um projeto do zero para identificar e otimizar o Kenshi Mod Manager (KMM) do jogo Kenshi, porque isso vai me deixar exercitar as tarefas que preciso em meu trabalho e o risco é mais controlado.
Resumidamente o gerenciador tem vários recursos, mas quando desenvolvi há cerca de um ano, haviam problemas sérios de performance, bom… não pro meu pc, mas pra quem utiliza.
Tenho cerca de 300+ mods, e indexar os conflitos nele causam ~1GBs e um tempo considerável pra apresentar as falhas
Como que comecei a análise
O KMM é um WPF em .Net Core 3.1 e eu acho realmente muito trabalhoso analisar cada parte do código sem ter uma ferramenta externa, já que o projeto já passou de 5K de linhas..
Então já que eu precisava estudar gRPC iniciei primeiro criando dois projetos, um server e um client.
sendo que o server está no KMM e o client em um console no MMDHelpers.
A alteração no KMM foi muito simples, bom … já que ele não era uma aplicação web, eu apenas subi o kestrel.
Uma observação, neste webserver que eu estou subindo, estou ignorando a parte de segurança completamente, já que irei identificar os problemas, remover o pacote e disponibilizar a nova versão.
Com o Client eu pude enviar comando para iniciar ou terminar as métricas no KMM
o resultado foi este:
Eu consigo observar quantas coletas em Gen0-2, a quantidade de memória e o tempo que levou.
o tempo aqui não é meu foco ainda, mais para abaixo explico uma otimização para tempo (mas é tiro de canhão pra matar mosca)
Primeiro Passo
Particularmente eu tento quebrar os problemas mais complexos para o mais simples possível, e existe um item que não tinha nenhuma coleta no GC mas era o primeiro item que eu queria otimizar
Temos aqui o cenário:
O código nem causa problema, afinal só estou usando o IO.Path.GetExtension e comparando se o arquivo está naquela extensão.
então tinha duas opções:
- Usar String.EndsWith
- Criar o meu EndsWith
Ainda estou encontrando formas de pegar o código base do c# pra remover alguns recursos que não preciso validar, ganhando tempo de processamento.
Em minha solução eu achei mais fácil iterar uma string do final pro começo, levando em conta que se qualquer char for diferente ele retorna false,
o código ficou desse jeito:
A única coisa que faltou foi um benchmark certo?
então tá ai:
não apenas descartei o EndsWith de string, como percebi que minha versão é mais eficiente pro problema um ganho de 69% de performance, se este problema estivesse em um cenário de 4M de iterações o ganho seria de 195.16ms para 60.44ms, Bom depende muito do cenário né? mas… é algo á se pensar quando for otimizar, aqui nem usei o Princípio de Pareto, só foi pra brincar mesmo.
Pontos de atenção:
no meu cenário não preciso verificar se a string é case-sensitive ou possui acentos.
Segundo Passo
Fiz uma dúzia de alterações simples:
- Parei de usar List para um array pequeno onde eu não precisava fazer nada apenas conter a lista.
- Alterei algumas classes para structs pois as classes eram imutáveis e também não há risco de cópia em memória delas.
- adicionei o capacity em algumas até o momento onde revisei
O que seria dar essa lista sem o commit não é mesmo? segue ai:
https://github.com/millerscout/Kenshi-Mod-Manager/commit/7f9ef386bdcb902ed3a168afac8eaaa23babf4c1
o resultado foi bem promissor:
Gen 0: 990 redução de 18% comparando com 1217 anteriormente
Gen 1: 284 redução de 31% comparando com 351 anteriormente
Gen 3: 28 aumento de 16% comparando com 24 anteriormente
Primeira parte encerrada aqui.
Você pode continuar lendo a Parte 2
Conclusão
Ainda há um longo caminho de aprendizado e técnicas para aprimorar minha capacidade de fine-tuning em .Net, mas acredito que o resultado mostra que não é rocket science, apenas precisamos nos policiar em coisas simples para trazer um ganho maior.
Créditos
Acredito que essa minha vontade de otimização não seriam possíveis caso as conversas com o @crocco @Massato não tivessem acontecido.
Eu tive também alguns materiais para me inspirar e aprender, bem como algumas referências, sendo elas:
o tio Scott, Elemar Jr. e Konrad Kokosa
Estive consumindo vários artigos da Eximia.Co, documentações da própria microsoft em momentos específicos os seguintes itens:
- High-performance code design patterns in C#. Konrad Kokosa .NET Fest 2019
- Micro-otimizações em código .NET – Quando e como fazer? Como avaliar?
- Processando arquivos grandes (CSV) em .NET
- Melhorando a performance de aplicações .NET com “Value Types” bem implementados
- Como funciona o “yield return”, do C#, “por baixo do capô”?
- Paralelismo em .NET com Thread, ThreadPool, TPL e CUDA
- Como o Garbage Collector (GC) afeta a performance em .NET: Validação de CPF
- Entendendo a Heap e o Garbage Collector em .NET
- Pequenas mudanças no código, grandes ganhos (10x) na performance em aplicações .NET
- C# Async/Await/Task Explained (Deep Dive)
Sneak Peek
Próximo post gostaria de experimentar mais sobre sobre warmup em .Net, comecei ler algumas coisas, assim que tiver algo conclusivo trago para compartilhar 🙂