CodeGym /Cours /C# SELF /Optimisation des performances : ValueTask

Optimisation des performances : ValueTask

C# SELF
Niveau 62, Leçon 2
Disponible

1. Introduction

Avant de plonger dans le code, expliquons simplement. Souviens-toi qu'instancier un objet en .NET (et encore plus un task !) coûte de la mémoire et un peu de CPU. Imagine une API asynchrone qui dans la moitié des cas renvoie le résultat immédiatement (par ex. depuis un cache), et dans l'autre moitié interroge la base de données, donc l'opération devient vraiment asynchrone et nécessite un Task. On rencontre ça souvent — caches de fichiers, API réseau, object pools et autres scénarios "asynchrones, mais parfois instantanés".

Si on retourne toujours un Task, même pour les résultats instantanés il faudra créer des objets en trop. Et si on pouvait renvoyer le résultat sans task quand il est déjà prêt ? C'est exactement pour ça que ValueTask existe.

Fait : les habituels Task.CompletedTask et Task.FromResult(…) économisent du temps d'exécution, mais créent un objet partagé qui peut ne pas être idéal pour des scénarios à haute charge.

Qu'est-ce que ValueTask ?

ValueTask est une structure-wrapper qui peut représenter soit une valeur déjà prête, soit (si l'opération est réellement asynchrone) le task lui‑même. En clair, c'est un "paquet" qui peut contenir soit directement le résultat, soit une référence à un Task.

Il y a deux variantes principales :

  • ValueTask — sans valeur de retour (comme Task sans résultat)
  • ValueTask<TResult> — wrapper pour une valeur (analogue à Task<TResult>)

Comparaison Task vs ValueTask

Type Nombre d'allocations Peut être complété synchroniquement Utilisation typique
Task
Une (heap) Oui/Non Presque toujours
ValueTask
Zéro/une Oui/Non Optimisation
ValueTask<T>
Zéro/une Oui/Non Optimisation

Quand utiliser ValueTask

Il y a une règle d'or : si tu écris une méthode asynchrone qui retourne toujours son résultat après une opération async, utilise Task. C'est simple, sûr et compréhensible de tous.

Utilise ValueTask quand :

  • Le résultat peut être obtenu synchroniquement (par ex. depuis un cache, un pool, la mémoire), et tu veux économiser des allocations inutiles.
  • L'opération asynchrone arrive rarement (sinon le gain est perdu à cause de la complexité du code et des copies de structures).

Attention ! Si tu retournes toujours un résultat asynchrone — utilise sans hésiter Task. Si tu veux que ton API soit "hyper-optimisée" pour les cas fréquents de résultats instantanés — alors ValueTask.

2. Résultat synchrone ou asynchrone

Regardons une fonction qui cherche un utilisateur par nom. Si l'utilisateur est dans le cache — on le retourne immédiatement ; sinon on le charge de manière asynchrone depuis la "base" :

// On modélise notre utilisateur
public class User
{
    public string Name { get; set; }
}

// Notre cache (très simple)
private readonly Dictionary<string, User> _localCache = new();

public async ValueTask<User> FindUserAsync(string name)
{
    // On vérifie le cache local
    if (_localCache.TryGetValue(name, out var user))
    {
        // Résultat instantané — pas d'allocation de task !
        return user;
    }

    // Ici une opération asynchrone supposée longue (par ex. depuis la base de données)
    user = await LoadUserFromDbAsync(name);
    // On met en cache pour la prochaine fois
    _localCache[name] = user;
    return user;
}

private async Task<User> LoadUserFromDbAsync(string name)
{
    // On simule un délai
    await Task.Delay(500);
    return new User { Name = name };
}

Important : En cas de cache-hit on retourne le résultat directement — aucune allocation de task ! Ce n'est que lorsque le résultat doit être "récupéré" qu'on crée une véritable tâche asynchrone.

Comment ValueTask est structuré à l'intérieur

Un ValueTask contient soit une valeur prête, soit une référence à un Task :

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

Quand tu utilises await, le compilateur s'en occupe : si le résultat est instantané, il n'y aura pas d'objets supplémentaires créés.

Important à propos de await et ValueTask

Les méthodes async s'entendent très bien avec await pour ValueTask :

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

Mais si tu conserves une instance de ValueTask et décides de l'attendre plus tard, souviens-toi : tu ne dois pas faire await du même exemplaire plus d'une fois. Un Task peut être attendu plusieurs fois, pas un ValueTask.

Erreur : refaire un await sur un ValueTask

ValueTask<int> task = ComputeAsync();

// C'est OK
int a = await task;

// C'est une erreur ! Faire un second await sur la même instance de ValueTask n'est pas permis :
// int b = await task; // INTERDIT !

3. Réécrivons une partie de notre application console

Supposons qu'on développe maintenant un mini-lecteur de livres : une partie des textes est déjà en cache, le reste est téléchargé de façon asynchrone. Implémentons une méthode optimisée pour récupérer la première ligne d'un livre avec 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;
    }

    // Supposons un téléchargement asynchrone du livre
    var downloadedBook = await DownloadBookTextAsync(title);
    _bookCache[title] = downloadedBook;
    return downloadedBook.Split('\n')[0];
}

private async Task<string> DownloadBookTextAsync(string title)
{
    // On simule un délai (par ex. téléchargement depuis Internet)
    await Task.Delay(1000);
    return $"Book: {title}\nThis is the first line.\nSecond line...";
}

Voilà comment ValueTask évite la création d'un task quand le livre est déjà en cache.

4. Petits détails utiles

Marqueur : quand ne pas utiliser ValueTask

Si ton API est toujours asynchrone (par ex. toujours appel réseau) — utilise Task, c'est plus simple et sûr.

Comment convertir ValueTask en Task et inversement

Il arrive qu'un ValueTask "ne rentre pas" dans une API qui attend un Task. Utilise .AsTask() :

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

Si tu veux faire l'inverse (créer un ValueTask à partir d'un Task) — crée simplement un ValueTask à partir du task :

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

Comparaison : ValueTask vs Task

Caractéristique
Task
ValueTask
Second await Permis Interdit
Allocations pour réponses rapides Présentes Absentes (si résultat instantané)
Compatibilité d'interface Largement supporté Nécessite des wrappers supplémentaires
Applicabilité Partout Uniquement pour optimisation
Pooling (Pool) Utilise un pool global Non, c'est une structure
Simplicité Simple Plus complexe

Utiliser ValueTask en pratique

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;
}

À mentionner en entretien : si on te demande comment booster encore les perfs d'une API asynchrone qui fait du caching, parle de ValueTask.

Exemples de compatibilité avec LINQ et IAsyncEnumerable<T>

Si tu veux utiliser ValueTask avec le LINQ asynchrone (IAsyncEnumerable<T>), c'est supporté en .NET (par ex. la méthode ToListAsync) :

public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
    // On simule que certaines valeurs sont déjà calculées, d'autres viennent d'opérations asynchrones
    // ... (exemple omis pour la concision)
    return new List<int> { 2, 3, 5, 7, 11 };
}

5. Conclusions et particularités d'implémentation

  1. Utilise ValueTask pour optimiser quand une part significative des résultats est prête instantanément (par ex. venant d'un cache).
  2. N'utilise pas ValueTask "pour rien" — ça complique le code et peut introduire des bugs.
  3. Ne fais pas de await sur un ValueTask plusieurs fois. Si besoin, convertis-le en Task.
  4. Toute la compatibilité avec Task se fait via .AsTask().
  5. Rappelle-toi que ValueTask est un struct, et peut agir différemment quand on le copie.

6. Erreurs typiques en travaillant avec ValueTask

Erreur n°1 : refaire un await sur la même instance de ValueTask. ValueTask est une structure, on ne peut pas l'awaiter deux fois. Le second await provoquera une exception ou un comportement incorrect.

Erreur n°2 : utiliser ValueTask sans vérifier la compatibilité. Certains APIs tiers exigent un Task, et passer un ValueTask directement peut poser problème.

Erreur n°3 : utiliser ValueTask sans besoin. Si le résultat est toujours asynchrone, ValueTask complique le code sans bénéfice réel.

Erreur n°4 : copier la structure ValueTask. Une structure copiée peut se comporter de façon inattendue lors d'un await, travaille toujours avec l'original quand c'est possible.

2
Mission
C# SELF, niveau 62, leçon 2
Bloqué
Renvoyer le résultat instantanément ou de façon asynchrone
Renvoyer le résultat instantanément ou de façon asynchrone
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION