Melhorando a performance de aplicações .NET com “Value Types” bem implementados

Esta publicação está disponível em vídeo, ampliada e revisada, no canal da EximiaCo.


Neste post, vamos abordar como implementar “Value Types” corretamente e melhorar a performance de nossas aplicações.

NOTA: Este post foi escrito, originalmente, em inglês. Esta tradução foi produzida a pedido de um leitor. Se você gosta do nosso conteúdo e gostaria de solicitar a tradução de outros posts para Português, Inglês ou Espanhol, use os links disponíveis no cabeçalho.

Os exemplos foram adaptados do livro Pro .NET Performance. Eu tentei fazer com que eles fossem um pouco mais “realistas”, considerando minha experiência de quase 20 anos escrevendo tipos Point3.

Class ou Struct?

Sempre que criamos um tipo, temos duas opções:

  1. Criar um tipo de referência (class)
    • Fornece um conjunto mais rico de features, como:  herança, poder ser usado como “lock object”, etc.
    • Facilmente referenciado em múltiplas variáveis;
    • Igualdade por referência
    • Alocação na Heap e coletados pelo garbage collector
    • Elementos de arrays de instâncias de classes são apenas referências para objetos na heap.
    • Melhor para objetos grandes
  2. Criar um tipo de valor (struct)
    • Igualdade estrutural
    • Alocadas na stack ou aninhados com os tipos que as contem
    • Desalocado na quando seu na saída dos contextos
    • Elementos dos arrays de value types são os próprios valores (acesso com menos faltas de memória) .
    • Na maioria dos casos, funciona melhor com as estratégias de caching de CPU
    • devem ser implementadas para que sejam imutáveis (embora existam notórias exceções)
    • Instâncias devem ser objetos pequenos

Há prós e contras em ambas as opções. Geralmente, value types são menos poderosos, ideias para “objetos de valor DDD”, economizar memória e melhorar performance.

Quanto de melhoria de performance é possível com o uso correto de Value Types?

Depende! 🙂

Considere o seguinte programa:

using System;
using System.Collections.Generic;

namespace MyApp
{
    class Program
    {
        static void Main(string[] args)
        {
            const int numberOfPoints = 10_000_000;

            var points = new List<Point3>(numberOfPoints);
            for (var i = 0; i < numberOfPoints; i++)
            {
                points.Add(new Point3
                {
                    X = i, Y = i, Z = i
                });
            }

            Console.WriteLine($"{points.Count} points created.");
            Console.WriteLine("Press Any Key To Exit!");
            Console.ReadLine();
        }
    }

    public class Point3
    {
        public double X;
        public double Y;
        public double Z;
    }
}

Na minha máquina, esse programa aloca ~430 MB de RAM. Mesmo código, reescrito como structs aloca ~231MB. Economia de quase 200MB! Nada mal.

Necessidade para uma melhor implementação de Equals

Com uma lista de 10.000.000 de pontos, vamos tentar encontrar um ponto não-existente

var before = GC.CollectionCount(0);
var pointToFind = new Point3{ X = -1, Y = -1, Z = -1};

var sw = Stopwatch.StartNew();
var contains = points.Contains(pointToFind);
sw.Stop();

Console.WriteLine($"Time .: {sw.ElapsedMilliseconds} ms");
Console.WriteLine($"# Gen0: {GC.CollectionCount(0) - before}");

A tela a seguir foi o output em meu computador.

Nada mal, afinal estamos procurando um ponto em uma lista com 10.000.000 de elementos, Mas…

Olhando a implementação da Microsoft, List<T> usa o método Equals para fazer as comparações. Aqui está a  implementação atual  desse método para ValueType.

public override bool Equals(Object obj)
{
    if (null == obj)
    {
        return false;
    }
    RuntimeType thisType = (RuntimeType)this.GetType();
    RuntimeType thatType = (RuntimeType)obj.GetType();

    if (thatType != thisType)
    {
        return false;
    }

    Object thisObj = (Object)this;
    Object thisResult, thatResult;

    // if there are no GC references in this object we can avoid reflection 
    // and do a fast memcmp
    if (CanCompareBits(this))
        return FastEqualsCheck(thisObj, obj);

    FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

    for (int i = 0; i < thisFields.Length; i++)
    {
        thisResult = ((RtFieldInfo)thisFields[i]).UnsafeGetValue(thisObj);
        thatResult = ((RtFieldInfo)thisFields[i]).UnsafeGetValue(obj);

        if (thisResult == null)
        {
            if (thatResult != null)
                return false;
        }
        else
        if (!thisResult.Equals(thatResult))
        {
            return false;
        }
    }

    return true;
}

A implementação genérica se afasta muito do “mais otimizado possível”.  (Não use reflection!)

Tentemos uma implementação melhor.

public struct Point3
{
    public double X;
    public double Y;
    public double Z;

    public override bool Equals(object obj)
    {
        if (!(obj is Point3)) return false;
        var other = (Point3)obj;
        return
            Math.Abs(X - other.X) < 0.0001 &&
            Math.Abs(Y - other.Y) < 0.0001 &&
            Math.Abs(Z - other.Z) < 0.0001;
    }
}

Agora, essa é o output em minha máquina:

Muito melhor!

Evitando boxing

Você viu que tivemos mais de 100 coletas em gen #0? Por que elas ocorreram, considerando que estamos usando uma struct?

Equals , por padrão, recebe um objeto como parâmetro. Logo, sempre que passamos um value type .NET irá precisar mover o objeto para a heap (em uma operação conhecida como boxing)

No nosso exemplo, estamos comparando um value type com  10.000.000 de instâncias. Logo, temos 10.000.000 de objetos sendo “boxed”. Para impedir isso, podemos implementar  a interface IEquatable.

public struct Point3 : IEquatable<Point3>
{
    public double X;
    public double Y;
    public double Z;

    public override bool Equals(object obj)
    {
        if (!(obj is Point3 other))
        {
            return false;
        }

        return 
            Math.Abs(X - other.X) < 0.0001 &&
            Math.Abs(Y - other.Y) < 0.0001 &&
            Math.Abs(Z - other.Z) < 0.0001;
    }

    public bool Equals(Point3 other) => 
        Math.Abs(X - other.X) < 0.0001 &&
        Math.Abs(Y - other.Y) < 0.0001 &&
        Math.Abs(Z - other.Z) < 0.0001;

    public static bool operator ==(Point3 a, Point3 b) => a.Equals(b);

    public static bool operator !=(Point3 a, Point3 b) => !a.Equals(b);
}

Resultado, agora:

Sem GC!

Pensando em uma melhor implementação de  GetHashCode

Se você usa seus objetos como chaves de dicionários, considere investir algum tempo aprendendo como escrever melhores implementações para GetHashCode. Esta thread no StackOverflow é um bom ponto de partida 

Mãos a obra

Performance é uma feature! Usar Value Types podem ajudar você a melhorar a performance de sua aplicação dramaticamente. Logo, se você cria tipos sempre usando class, pare agora!

NOTA: Eu investi quase 20 anos de minha carreira escrevendo sistemas CAD. Não tenho ideia de quantas vezes implementei Point3. Entretanto, tenho um pequeno segredo: por muito tempo, criei Point3 como classe. Você não está sozinho!

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
4 Comentários
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Alexandre Brandão Lustosa
Alexandre Brandão Lustosa
4 anos atrás

Sensacional, muito melhor mesmo. Vou considerar fortemente em meus projetos. Obrigado pelo post.

Leandro
Leandro
4 anos atrás

Excelente! Demais! Valeu demais por compartilhar!

Daniel
Daniel
3 anos atrás

Muito bacana seus exemplos. Traga sempre mais 🙂

Bruno
Bruno
3 anos atrás

Elemar, o artigo é muito bacana. Porém, a imagens de resultados não estão aparecendo, poderia verificar por gentileza?

AUTOR

Elemar Júnior
Fundador e CEO da EximiaCo atua como tech trusted advisor ajudando empresas e profissionais a gerar mais resultados através da tecnologia.

NOVOS HORIZONTES PARA O SEU NEGÓCIO

Nosso time está preparado para superar junto com você grandes desafios tecnológicos.

Entre em contato e vamos juntos utilizar a tecnologia do jeito certo para gerar mais resultados.

Insights EximiaCo

Confira os conteúdos de negócios e tecnologia desenvolvidos pelos nossos consultores:

Arquivo

Pós-pandemia, trabalho remoto e a retenção dos profissionais de TI

CTO Consulting e Especialista em Execução em TI
EximiaCo 2024 - Todos os direitos reservados
4
0
Queremos saber a sua opinião, deixe seu comentáriox
()
x

Muito obrigado!

Deu tudo certo com seu envio!
Logo entraremos em contato

Melhorando a performance de aplicações .NET com “Value Types” bem implementados

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Melhorando a performance de aplicações .NET com “Value Types” bem implementados

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Condição especial de pré-venda: R$ 14.000,00 - contratando a mentoria até até 31/01/2023 e R$ 15.000,00 - contratando a mentoria a partir de 01/02/2023, em até 12x com taxas.

Tenho interesse nessa capacitação

Para solicitar mais informações sobre essa capacitação para a sua empresa, preencha o formulário a seguir:

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse nessa solução

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse neste serviço

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

× Precisa de ajuda?