Olá a todos! Este artigo é uma coleção de práticas recomendadas que meus colegas e eu usamos há muito tempo ao trabalhar em diferentes projetos.
Informações sobre a máquina na qual os cálculos foram realizados:BenchmarkDotNet=v0.11.5, SO=Windows 10.0.18362
CPU Intel Core i5-8250U 1.60 GHz (Kaby Lake R), 1 CPU, 8 núcleos lógicos e 4 físicos
SDK do .NET Core=3.0.100
[Host]: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), RyuJIT de 64 bits
Núcleo: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), RyuJIT de 64 bits
[Host]: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), RyuJIT de 64 bits
Núcleo: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), RyuJIT de 64 bits
Trabalho = Núcleo Tempo de execução = Núcleo
ToList vs ToArray e Ciclos
Planejei preparar essas informações com o lançamento do .NET Core 3.0, mas eles me venceram, não quero roubar a glória de outra pessoa e copiar as informações de outras pessoas, então vou apenas salientar link para um bom artigo onde a comparação é descrita em detalhes.
Em meu nome, quero apenas apresentar a vocês minhas medições e resultados; adicionei loops reversos a eles para os amantes do “estilo C++” de escrever loops.
Código:
public class Bench
{
private List<int> _list;
private int[] _array;
[Params(100000, 10000000)] public int N;
[GlobalSetup]
public void Setup()
{
const int MIN = 1;
const int MAX = 10;
Random random = new Random();
_list = Enumerable.Repeat(0, N).Select(i => random.Next(MIN, MAX)).ToList();
_array = _list.ToArray();
}
[Benchmark]
public int ForList()
{
int total = 0;
for (int i = 0; i < _list.Count; i++)
{
total += _list[i];
}
return total;
}
[Benchmark]
public int ForListFromEnd()
{
int total = 0;t
for (int i = _list.Count-1; i > 0; i--)
{
total += _list[i];
}
return total;
}
[Benchmark]
public int ForeachList()
{
int total = 0;
foreach (int i in _list)
{
total += i;
}
return total;
}
[Benchmark]
public int ForeachArray()
{
int total = 0;
foreach (int i in _array)
{
total += i;
}
return total;
}
[Benchmark]
public int ForArray()
{
int total = 0;
for (int i = 0; i < _array.Length; i++)
{
total += _array[i];
}
return total;
}
[Benchmark]
public int ForArrayFromEnd()
{
int total = 0;
for (int i = _array.Length-1; i > 0; i--)
{
total += _array[i];
}
return total;
}
}
As velocidades de desempenho no .NET Core 2.2 e 3.0 são quase idênticas. Aqui está o que consegui obter no .NET Core 3.0:
Podemos concluir que o processamento iterativo de uma coleção Array é mais rápido devido às suas otimizações internas e à alocação explícita do tamanho da coleção. Também vale lembrar que uma coleção List tem suas vantagens e você deve usar a coleção certa dependendo dos cálculos necessários. Mesmo se você escrever lógica para trabalhar com loops, não esqueça que este é um loop comum e também está sujeito a uma possível otimização de loop. Um artigo foi publicado no habr há muito tempo: https://habr.com/ru/post/124910/. Ainda é uma leitura relevante e recomendada.
Jogar
Há um ano trabalhei em uma empresa em um projeto legado, nesse projeto era normal processar a validação de campo através de uma construção try-catch-throw. Eu já entendi então que essa lógica de negócio não era saudável para o projeto, então sempre que possível tentei não usar tal design. Mas vamos descobrir por que a abordagem para lidar com erros com tal construção é ruim. Escrevi um pequeno código para comparar as duas abordagens e fiz benchmarks para cada opção.
Código:
public bool ContainsHash()
{
bool result = false;
foreach (var file in _files)
{
var extension = Path.GetExtension(file);
if (_hash.Contains(extension))
result = true;
}
return result;
}
public bool ContainsHashTryCatch()
{
bool result = false;
try
{
foreach (var file in _files)
{
var extension = Path.GetExtension(file);
if (_hash.Contains(extension))
result = true;
}
if(!result)
throw new Exception("false");
}
catch (Exception e)
{
result = false;
}
return result;
}
Os resultados no .NET Core 3.0 e Core 2.2 têm um resultado semelhante (.NET Core 3.0):
Try catch torna o código mais difícil de entender e aumenta o tempo de execução do seu programa. Mas se você precisar dessa construção, não deverá inserir aquelas linhas de código que não devem lidar com erros - isso tornará o código mais fácil de entender. Na verdade, não é tanto o tratamento de exceções que carrega o sistema, mas sim o lançamento dos próprios erros por meio da construção throw new Exception.
Lançar exceções é mais lento do que alguma classe que coletará o erro no formato necessário. Se você está processando um formulário ou alguns dados e sabe claramente qual deve ser o erro, por que não processá-lo?
Você não deve escrever uma construção throw new Exception() se esta situação não for excepcional. Manipular e lançar uma exceção é muito caro!!!
Ao longo dos meus 5 anos de experiência trabalhando na plataforma .NET, encontrei muitos projetos que usavam correspondência de strings. Também vi a seguinte imagem: havia uma solução empresarial com muitos projetos, cada um realizando comparações de strings de maneira diferente. Mas o que deve ser usado e como unificá-lo? No livro CLR via C# de Richter, li informações de que o método ToUpperInvariant() é mais rápido que ToLowerInvariant().
Trecho do livro:
Claro, eu não acreditei e resolvi fazer alguns testes no .NET Framework e o resultado me chocou - mais de 15% de aumento de desempenho. Então, ao chegar ao trabalho na manhã seguinte, mostrei essas medidas aos meus superiores e dei-lhes acesso ao código-fonte. Depois disso, 2 dos 14 projetos foram alterados para acomodar as novas medidas, e considerando que esses dois projetos existiam para processar enormes tabelas Excel, o resultado foi mais que significativo para o produto.
Também apresento medidas para diferentes versões do .NET Core, para que cada um de vocês possa escolher a solução mais ideal. E só quero acrescentar que na empresa onde trabalho usamos ToUpper() para comparar strings.
Código:
public const string defaultString = "VXTDuob5YhummuDq1PPXOHE4PbrRjYfBjcHdFs8UcKSAHOCGievbUItWhU3ovCmRALgdZUG1CB0sQ4iMj8Z1ZfkML2owvfkOKxBCoFUAN4VLd4I8ietmlsS5PtdQEn6zEgy1uCVZXiXuubd0xM5ONVZBqDu6nOVq1GQloEjeRN8jXrj0MVUexB9aIECs7caKGddpuut3";
[Benchmark]
public bool ToLower()
{
return defaultString.ToLower() == defaultString.ToLower();
}
[Benchmark]
public bool ToLowerInvariant()
{
return defaultString.ToLowerInvariant() == defaultString.ToLowerInvariant();
}
[Benchmark]
public bool ToUpper()
{
return defaultString.ToUpper() == defaultString.ToUpper();
}
[Benchmark]
public bool ToUpperInvariant()
{
return defaultString.ToUpperInvariant() == defaultString.ToUpperInvariant();
}
No .NET Core 3.0, o aumento para cada um desses métodos é ~x2 e equilibra as implementações entre si.
Compilação de camadas
No meu último artigo descrevi brevemente esta funcionalidade, gostaria de corrigir e complementar minhas palavras. A compilação multinível acelera o tempo de inicialização da sua solução, mas você sacrifica o fato de que partes do seu código serão compiladas em uma versão mais otimizada em segundo plano, o que pode introduzir uma pequena sobrecarga. Com o advento do NET Core 3.0, o tempo de compilação para projetos com compilação em camadas habilitada diminuiu e os bugs associados a essa tecnologia foram corrigidos. Anteriormente, essa tecnologia gerava erros nas primeiras solicitações no ASP.NET Core e travava durante a primeira compilação no modo de compilação multinível. Atualmente está habilitado por padrão no .NET Core 3.0, mas você pode desativá-lo se desejar. Se você está na posição de líder de equipe, sênior, intermediário ou chefe de departamento, então deve entender que o rápido desenvolvimento do projeto aumenta o valor da equipe e esta tecnologia permitirá que você economize tempo para ambos os desenvolvedores e o tempo do projeto em si.
Nível .NET subindo
Atualize sua versão do .NET Framework/.NET Core. Muitas vezes, cada nova versão proporciona ganhos adicionais de desempenho e adiciona novos recursos.
Mas quais são exatamente os benefícios? Vejamos alguns deles:
O .NET Core 3.0 introduziu imagens R2R que reduzirão o tempo de inicialização dos aplicativos .NET Core.
Com a versão 2.2 surgiu o Tier Compilation, graças ao qual os programadores gastarão menos tempo lançando um projeto.
Suporte para novos padrões .NET.
Suporte para uma nova versão da linguagem de programação.
Otimização, a cada nova versão a otimização das bibliotecas base Collection/Struct/Stream/String/Regex e muito mais melhora. Se você estiver migrando do .NET Framework para o .NET Core, obterá um grande aumento de desempenho imediatamente. Como exemplo, anexo um link para algumas das otimizações que foram adicionadas ao .NET Core 3.0: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core-3-0/
Conclusão
Ao escrever o código, vale a pena prestar atenção aos diferentes aspectos do seu projeto e utilizar os recursos da sua linguagem de programação e plataforma para obter o melhor resultado. Ficarei feliz se você compartilhar seu conhecimento relacionado à otimização em .NET.