Fundamentos para Performance

ValueTuple vs Tuple

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.

Para saber mais sobre os impactos de escolher classes ou structs, recomendamos a leitura do post que escrevemos sobre esse tema.

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.

Mais posts da série Fundamentos para Performance

1 comentário
  1. Vinícius Mamoré Caldeira de Oliveira

    Muito interessante, obrigado pelo post!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *