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!