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:
- 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
- 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!
Sensacional, muito melhor mesmo. Vou considerar fortemente em meus projetos. Obrigado pelo post.
Excelente! Demais! Valeu demais por compartilhar!
Muito bacana seus exemplos. Traga sempre mais 🙂
Elemar, o artigo é muito bacana. Porém, a imagens de resultados não estão aparecendo, poderia verificar por gentileza?