0Pricing
C# Academy · レッスン

ValueTaskとアロケーションの回避

ホットパスでのヒープアロケーションを減らすためにValueTaskを使い、その制約を理解して、よくある誤用を避けます。

「ValueTaskとアロケーションの回避」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。

Taskのアロケーション問題

Task<T>は参照型です。結果を同期的に取得できる場合(キャッシュから取得する場合など)でも、すべてのasyncメソッドはヒープ上にTaskをアロケートします。頻繁に実行される処理では、これがGCへの大きな負荷になることがあります。ValueTask<T>はこの問題を解決します。

ValueTaskとは

ValueTask<T>は、同期的に利用可能な結果または保留中の非同期操作のいずれかを表せる構造体です。結果が同期的に得られる場合、ヒープへのアロケーションは必要ありません。

// Task<T>: always allocates, even when result is cached
public async Task<string> GetCached_Task() =>
    _cache.TryGetValue("key", out var v) ? v : await FetchAsync();

// ValueTask<T>: zero allocation when value is cached
public ValueTask<string> GetCached_ValueTask() =>
    _cache.TryGetValue("key", out var v)
        ? ValueTask.FromResult(v)  // no allocation
        : new ValueTask<string>(FetchAsync()); // allocation only when needed

インターフェースからのValueTaskの返却

頻繁に呼び出され、同期的に完了することが多いインターフェースメソッドでは、ValueTask<T>を使用します。利用側では、Task<T>とまったく同じように扱えます。

public interface IProductRepository
{
    // ValueTask: often completes synchronously from cache
    ValueTask<Product?> GetByIdAsync(int id);

    // Task: always async (DB query)
    Task<List<Product>> SearchAsync(string query);
}

// Consumption is identical to Task:
var product = await _repo.GetByIdAsync(42);
var results = await _repo.SearchAsync("keyboard");

IValueTaskSourceを使ったValueTask

極めて高いパフォーマンスが必要な場合は、IValueTaskSource<T>を実装して、基盤となる状態オブジェクトをプールします。これにより、非同期完了の場合でもすべてのアロケーションを回避できます。.NETのソケットやI/Oコードで内部的に使用されている方法です。

// In most cases, use the simple ValueTask approach.
// IValueTaskSource is for library authors needing zero-alloc async at extreme scale.

// Example: System.IO.Pipelines uses this internally
var result = await pipe.Reader.ReadAsync(ct);
// ReadAsync returns ValueTask<ReadResult> backed by IValueTaskSource
// -- zero allocation for every read even under high throughput

ValueTaskの制約:awaitは1回だけ

重要なルール:ValueTask<T>に対してawaitできるのは最大1回であり、完了前に.Resultを呼び出すこともできません。これに違反すると、未定義の動作が発生します。

var vt = repo.GetByIdAsync(42);

// BAD: await multiple times
var a = await vt; // OK
var b = await vt; // UNDEFINED BEHAVIOR — don't do this!

// BAD: two concurrent awaits
var t1 = vt.AsTask(); // converts to Task (safe to share)
var t2 = vt.AsTask(); // second AsTask() on same ValueTask — WRONG

// GOOD: if you need multiple awaits, convert to Task
var task = repo.GetByIdAsync(42).AsTask();
var r1 = await task;
var r2 = await task; // OK — Task can be awaited multiple times

ValueTask<T>とTask<T>の選択ガイド

非同期メソッドによるアロケーションの負荷があることをプロファイリングで確認できた場合に限り、ValueTask<T>を使用します。どこでも使うべきではありません。呼び出し頻度の低いコードでは、メリットがないまま複雑さだけが増します。

// USE ValueTask when:
// 1. The method often completes synchronously (cache hit)
// 2. The method is on a hot path called millions of times/sec
// 3. You're writing a library with high-throughput I/O

// USE Task when:
// 1. The result is always async
// 2. The method might be awaited multiple times
// 3. You use Task.WhenAll / Task.WhenAny
// 4. You store the task in a variable and share it

非ジェネリックValueTask

(非ジェネリックの)ValueTaskは、Taskを返すメソッド(戻り値のない非同期処理)で使用します。頻繁に呼び出され、同期的に完了することが多いメソッドに適しています。

public interface ICache
{
    // Often a no-op when value exists — ValueTask avoids allocation
    ValueTask SetAsync(string key, string value);
    ValueTask InvalidateAsync(string key);
}

public class InMemoryCache : ICache
{
    private readonly Dictionary<string, string> _data = new();

    public ValueTask SetAsync(string key, string value)
    {
        _data[key] = value;
        return ValueTask.CompletedTask; // no allocation
    }
}

BenchmarkDotNetによるアロケーションの測定

BenchmarkDotNetで[MemoryDiagnoser]を使用すると、操作ごとのアロケーションを測定できます。ValueTaskが実際に役立っていることを検証する唯一の信頼できる方法です。

[MemoryDiagnoser]
public class CacheBenchmark
{
    private readonly CachedRepo _repo = new();

    [Benchmark]
    public async Task<Product?> Task_GetById()
        => await _repo.GetByIdTask(1);

    [Benchmark]
    public async ValueTask<Product?> ValueTask_GetById()
        => await _repo.GetByIdValueTask(1);
}

// Run: dotnet run -c Release
// Compare Gen0 allocations column — ValueTask should show 0B on cache hit

SocketsHttpHandlerでのValueTask

.NETのHTTPスタックでは、内部でValueTaskを広範囲に使用しています。これを理解すると、ランタイムのアロケーション戦略に沿った高性能なHTTPクライアントを記述できるようになります。

// HttpClient uses ValueTask-based sockets internally.
// When using HttpClient.GetAsync(), the response is awaited with Task<HttpResponseMessage>.
// For streaming large bodies, use:
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
await using var stream = await response.Content.ReadAsStreamAsync();
// Stream reads internally use ValueTask<int> for zero-allocation I/O

実践例:頻繁に実行されるキャッシュサービス

すべての呼び出しでキャッシュヒットの可能性があるキャッシュサービスでは、ValueTaskによって頻繁に実行される処理でのアロケーションをなくしながら、キャッシュミス時の非同期フォールバックにも対応できます。

public class ProductCache
{
    private readonly Dictionary<int, Product> _cache = new();
    private readonly IProductRepository _repo;

    public ProductCache(IProductRepository repo) => _repo = repo;

    public ValueTask<Product?> GetAsync(int id)
    {
        // Cache hit: no allocation
        if (_cache.TryGetValue(id, out var cached))
            return ValueTask.FromResult<Product?>(cached);

        // Cache miss: async fetch
        return new ValueTask<Product?>(FetchAndCacheAsync(id));
    }

    private async Task<Product?> FetchAndCacheAsync(int id)
    {
        var product = await _repo.GetByIdAsync(id);
        if (product is not null) _cache[id] = product;
        return product;
    }
}

確認問題

ValueTaskに対してawaitする際の重要なルールは何ですか。

まとめ:ValueTaskとアロケーションの回避

重要なポイント:

  • ValueTask<T>:結果が同期的に得られる場合にヒープアロケーションを回避する構造体です
  • 頻繁に実行され、同期的に完了することが多い処理(キャッシュヒットなど)に適しています
  • ValueTask<T>に2回以上awaitすることは絶対に避けてください — 共有するには.AsTask()を使用します
  • 最適化の前に[MemoryDiagnoser]を使い、アロケーション削減効果を検証します
  • 一般的な用途ではTask<T>を優先し、プロファイリングで根拠が得られた場合に限りValueTask<T>を使用します

よくある質問

「ValueTaskとアロケーションの回避」レッスンは無料ですか?

はい。「ValueTaskとアロケーションの回避」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。

「ValueTaskとアロケーションの回避」で何を学びますか?

ホットパスでのヒープアロケーションを減らすためにValueTaskを使い、その制約を理解して、よくある誤用を避けます。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

C# Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「ValueTaskとアロケーションの回避」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このC# Academyレッスンでコードを書いて実行できますか?

はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. IAsyncEnumerableとawait foreach
  2. System.Threading.Channels
  3. ValueTaskとアロケーションの回避
  4. ConfigureAwaitとSynchronizationContext
← C# Academyに戻る