C# possui dois tipos diferentes para tuplas: ValueTuple e Tuple. O primeiro, ValueTuple, é uma struct e, por isso, por padrão, tem suas instâncias na stack. O segundo, Tuple, é uma classe e, por isso, tem suas instâncias na heap.
Tuple surgiu primeiro. ValueTuple veio depois para permitir ganhos de performance.
Tuple e ValueTuple na memória
Tuple, sendo uma classe (na heap), ocupa mais memória. Por exemplo, se criarmos uma instância de Tuple<float, float>, para representar um Point2, com coordenadas X e Y, este ocupará 16 bytes quando estivermos utilizando uma configuração de 32 bits e 24 bytes quando estivermos utilizando uma configuração de 64 bits.
ValueTuple, sendo uma struct (na stack), é mais limitada e ocupa menos memória. Seguindo o mesmo raciocínio que seguimos anteriormente, uma ValueTuple<float, float>, ocuparia apenas 8 bytes na memória, independente da configuração.
Tuple e ValueTuple no cache do processador
Atualmente, quase todos os processadores oferecem múltiplos níveis de caching para tornar o acesso a dados na memória mais rápido. Quanto mais próximo do processador estiver o cache, mais rápido o acesso (o acesso a memória RAM, pelo processador, é, geralmente 200x mais lento que o acesso. ao cache que está mais próximo do processador).
O cache mais próximo do processador costuma ser organizado em “linhas” de 64 bytes cada. Estrategicamente, o processador, ao buscar dados da memória, carrega dados adjacentes por assumir que esses dados serão utilizados na sequência. Se, por exemplo, tivermos arrays de Tuple<float, float> para processar, haverá espaço para quatro objetos no cache do processador quando estivermos rodando em 32 bits e dois quando estivermos rodando em 64 bits. Se estivermos rodando com ValueTuples<float, float>, teremos 8 objetos.
Essa diferença, aparentemente simples, implica em grandes diferenças em tempos de execução, como podemos ver no teste que segue:
using System; using System.Collections.Generic; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; namespace Tuples { public class Program { static void Main() { BenchmarkRunner.Run<SUT>(); } } public class SUT { public const int NUMBER_OF_TUPLES = 10000000; public static readonly List<Tuple<float, float>> SourceOfTuples = new List<Tuple<float, float>>(NUMBER_OF_TUPLES); public static readonly List<ValueTuple<float, float>> SourceOfValueTuples = new List<ValueTuple<float, float>>(NUMBER_OF_TUPLES); [GlobalSetup] public void GlobalSetup() { for (var i = 0; i < NUMBER_OF_TUPLES; i++) { SourceOfTuples.Add(new Tuple<float, float>(i, i)); SourceOfValueTuples.Add(new ValueTuple<float, float>(i, i)); } } [Benchmark] public float SumUsingTuples() { var sum = 0f; for (var i = 0; i < NUMBER_OF_TUPLES; i++) { sum += SourceOfTuples[i].Item1; } return sum; } [Benchmark] public float SumUsingValueTuples() { var sum = 0f; for (var i = 0; i < NUMBER_OF_TUPLES; i++) { sum += SourceOfValueTuples[i].Item1; } return sum; } } }
No teste, apenas somamos um dos elementos em duas listas – uma com Tuples e a outra com ValueTuples. Repare que não há qualquer inferência de GC visto que a carga acontece em um Setup e, não surpreendendo, a versão com ValueTuples foi 33% mais rápida.
Tuples vs ValueTuples e o Garbage Collector
Tuples são alocadas na heap, logo, impactam o GC. ValueTuples são alocadas na Stack, logo, não geram pressão sobre o GC a menos que passem por um processo de boxing.
Tuples vs ValueTuples e o .NET
Recentemente, a Microsoft adicionou a capacidade de funções em C# retornarem tuplas. Essas funções, na verdade, estão retornando ValueTuples. Um dos engenheiros responsáveis pela implementação fez uma série de excelentes posts explicando todo o embasamento dessa decisão em seu blog.
Desvantagens de ValueTuples
Todas as restrições conhecidas para structs estão impostas a ValueTuples. Há sempre de se considerar o custo de cópia sempre que um objeto no stack é passado para outro contexto; Não há suporte a multi-threading (sempre há cópia entre as threads, em contrapartida, não é necessário implementar qualquer tipo de gestão de concorrência).
Por enquanto … era isso
Nesse post fizemos uma breve apresentação do tipo ValueTuple e promovemos algumas comparações. Em posts futuros, falaremos mais sobre a decisão da Microsoft de usar structs em outros pontos chaves do framework indicando o que podemos aprender com a gigante de Redmond para melhorar nosso código.
Deixe suas impressões nos comentários.
Muito interessante, obrigado pelo post!