CodeGym /Cursos /C# SELF /Otimização de performance:

Otimização de performance: ValueTask

C# SELF
Nível 62 , Lição 2
Disponível

1. Introdução

Antes de cair no código, vamos entender de forma simples. Lembre-se que criar um objeto no .NET (e ainda mais — uma task!) custa memória e um pouco de tempo de CPU. Agora imagine uma API assíncrona que na metade das vezes retorna o resultado imediatamente (por exemplo, pega do cache) e na outra metade consulta o banco de dados, então a operação fica realmente assíncrona e precisa de um Task. Isso aparece com frequência — por exemplo, em caches de arquivo, APIs de rede, pools de objetos e outros cenários "assíncronos, mas às vezes instantâneos".

Se nós sempre retornarmos Task, mesmo para resultados instantâneos teremos que criar objetos desnecessários. E se pudéssemos retornar o resultado sem uma task quando ele já estiver pronto? Foi para isso que surgiu o ValueTask.

Fato: os padrões Task.CompletedTask e Task.FromResult(…) realmente economizam tempo de execução, mas criam um objeto compartilhado que pode não ser ideal em cenários de alta carga.

O que é ValueTask

ValueTask é uma struct-wrapper especial que pode representar ou um resultado já pronto, ou (se a operação for realmente assíncrona) a própria task. Em outras palavras, é um "pacote" que pode conter ou simplesmente o valor, ou uma referência para um Task.

Existem duas variantes principais:

  • ValueTask — "sem valor" (não retorna valor, como Task)
  • ValueTask<TResult> — wrapper para um valor (análogo a Task<TResult>)

Comparando Task e ValueTask

Tipo Número de alocações Pode ser concluído sincronicamente Usado normalmente
Task
Uma (heap) Sim/Não Quase sempre
ValueTask
Nenhuma/uma Sim/Não Otimização
ValueTask<T>
Nenhuma/uma Sim/Não Otimização

Quando aplicar ValueTask

Existe uma regra de ouro: se você escreve um método assíncrono comum que sempre retorna o resultado apenas depois de uma operação async, use Task. É simples, seguro e compreensível para todo mundo.

Use ValueTask quando:

  • O resultado pode ser obtido sincronicamente (por exemplo, do cache, pool, memória), e você quer economizar alocações desnecessárias.
  • A operação assíncrona acontece não com tanta frequência (caso contrário os benefícios se perdem frente à complexidade do código e cópia de structs).

Atenção! Se você sempre retorna um resultado assíncrono — use Task. Se você quer que sua API seja "super-otimizada" para resultados instantâneos frequentes — então ValueTask.

2. Resultado sincrono ou assíncrono

Vamos ver uma função que procura um usuário por nome. Se o usuário estiver no cache — retornamos instantaneamente; se não — carregamos assincronamente do "banco":


// Modelamos nosso usuário
public class User
{
    public string Name { get; set; }
}

// Nosso cache (muito simples)
private readonly Dictionary<string, User> _localCache = new();

public async ValueTask<User> FindUserAsync(string name)
{
    // Verificamos o cache local
    if (_localCache.TryGetValue(name, out var user))
    {
        // Resultado instantâneo — nenhuma alocação de task!
        return user;
    }

    // Aqui uma suposta operação assíncrona longa (por exemplo, do banco de dados)
    user = await LoadUserFromDbAsync(name);
    // Colocamos no cache para o futuro
    _localCache[name] = user;
    return user;
}

private async Task<User> LoadUserFromDbAsync(string name)
{
    // Simulamos atraso
    await Task.Delay(500);
    return new User { Name = name };
}

Importante: No caso de cache-hit retornamos o resultado diretamente — sem alocação de task! Só quando o resultado precisa ser "buscado" realmente criamos a tarefa assíncrona.

Como ValueTask é por dentro

Dentro do ValueTask pode estar ou um valor pronto, ou uma referência para um Task:

ValueTask result = ValueTask.CompletedTask;
ValueTask<int> valueResult = new ValueTask<int>(42);
ValueTask<int> valueResult2 = new ValueTask<int>(Task.Run(() => 42));

Ao usar await o compilador resolve automaticamente: se o resultado for instantâneo, não serão criados objetos extras.

Importante sobre await e ValueTask

Métodos async se dão bem com await para ValueTask:

public async ValueTask PingAsync()
{
    // ...
    await Task.Delay(10);
}

Mas se você guardar uma instância de ValueTask e decidir aguardá-la mais tarde, lembre-se: não é permitido fazer await da mesma instância mais de uma vez. Task pode ser aguardada múltiplas vezes, ValueTask não.

Erro: await repetido em ValueTask

ValueTask<int> task = ComputeAsync();

// Isso é OK
int a = await task;

// Isso é erro! Segundo await na mesma instância de ValueTask não é permitido:
// int b = await task; // NÃO PODE!

3. Reescrevendo parte do nosso aplicativo de console

Suponha que estamos desenvolvendo um mini-leitor de livros: parte dos textos já está carregada no cache, o resto é carregado assincronamente. Vamos implementar um método otimizado para obter a primeira linha do livro usando ValueTask:

private readonly Dictionary<string, string> _bookCache = new();

public async ValueTask<string> GetFirstLineOfBookAsync(string title)
{
    if (_bookCache.TryGetValue(title, out var bookText))
    {
        var firstLine = bookText.Split('\n')[0];
        return firstLine;
    }

    // Suponha que haja um download assíncrono do livro
    var downloadedBook = await DownloadBookTextAsync(title);
    _bookCache[title] = downloadedBook;
    return downloadedBook.Split('\n')[0];
}

private async Task<string> DownloadBookTextAsync(string title)
{
    // Simulamos atraso (por exemplo, download da internet)
    await Task.Delay(1000);
    return $"Book: {title}\nThis is the first line.\nSecond line...";
}

É assim que ValueTask economiza a criação de uma task quando o livro já está no cache.

4. Nuances úteis

Marcador: quando não usar ValueTask

Se sua API é sempre assíncrona (por exemplo, sempre comunica com a rede) — use Task, é mais simples e seguro.

Como transformar ValueTask em Task e vice-versa

Às vezes ValueTask "não cabe" numa API que espera Task. Use .AsTask():

ValueTask<int> valueTask = ComputeAsync();
Task<int> task = valueTask.AsTask();

Se precisar transformar um Task em ValueTask — crie um ValueTask a partir da task:

Task<int> task = ComputeAsyncTask();
ValueTask<int> valueTask = new ValueTask<int>(task);

Comparação: ValueTask vs Task

Característica
Task
ValueTask
Await repetido Permitido Não permitido
Alocações em respostas rápidas Existe Não (se o resultado for instantâneo)
Compatibilidade de interface Ampla suporte Requer wrappers adicionais
Aplicabilidade Em todo lugar Apenas para otimização
Pooling (Pool) Usa pool comum Não, é struct
Simplicidade Simples Mais complexo

Uso de ValueTask na vida real

public async ValueTask<string> GetMessageAsync(int id)
{
    if (_messageCache.TryGetValue(id, out var value))
        return value;

    var result = await LoadMessageFromDbAsync(id);
    _messageCache[id] = result;
    return result;
}

Aplicação em entrevista: Se te perguntarem como melhorar ainda mais a performance de uma API assíncrona com cache — mencione o ValueTask.

Exemplos de compatibilidade com LINQ e IAsyncEnumerable<T>

Se você quiser usar ValueTask com LINQ assíncrono (IAsyncEnumerable<T>), isso é suportado no .NET (por exemplo, o método ToListAsync):

public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
    // Simulamos que parte dos números já está calculada, e parte é operação assíncrona
    // ... (exemplo omitido por brevidade)
    return new List<int> { 2, 3, 5, 7, 11 };
}

5. Conclusões e características de implementação

  1. Use ValueTask para otimizar quando uma parte significativa do tempo seu resultado está pronta instantaneamente (por exemplo, do cache).
  2. Não use ValueTask "só por usar" — isso vai complicar o código e causar erros.
  3. Não await um ValueTask várias vezes. Se precisar, converta para Task.
  4. Toda a compatibilidade com Task é via .AsTask().
  5. Lembre que ValueTask é um struct, e pode se comportar diferente quando copiada.

6. Erros típicos ao trabalhar com ValueTask

Erro nº1: await repetido na mesma instância de ValueTask. ValueTask é uma struct, não se deve await-á-la duas vezes. O segundo await vai lançar exceção ou funcionar de forma incorreta.

Erro nº2: usar ValueTask sem checar compatibilidade. Alguns APIs externas exigem Task, e passar ValueTask diretamente pode causar problemas.

Erro nº3: usar ValueTask sem necessidade. Se o resultado é sempre assíncrono, usar ValueTask complica o código sem benefício real.

Erro nº4: copiar ValueTask como struct. Uma struct copiada pode se comportar de forma inesperada ao await; é importante trabalhar com o original.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION