mcpField
T
type-design-performance

type-design-performance

github.com/wshaddix開発8670 回インストール36 週間前に更新
リポジトリ

type-design-performance について

.NET 開発者向けの型設計スキルで、パフォーマンス、メモリ割り当て、API の形に関する指針を提供します。構造体、コレクション、列挙、非同期データ処理の選択を扱います。

主な機能

  1. 01構造体とクラスの選択指針
  2. 02型を原則 sealed にする方針
  3. 03readonly 構造体の設計要点
  4. 04コレクション型の選択指針
  5. 05遅延列挙と非同期ストリーム
  6. 06Span、Memory、ValueTask の使い方

活用シーン

  1. 01新しい型や API の設計
  2. 02コードのパフォーマンスレビュー
  3. 03class、struct、record の選択
  4. 04コレクションや列挙データの設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill type-design-performance

インストール後、Claude Code で/type-design-performanceを入力して読み込まれたか確認します。

詳細

パフォーマンスを考慮した型設計

このスキルを使用する場面

次のような場合に、このスキルを使用します。

  • 新しい型や API を設計するとき
  • パフォーマンス上の問題がないかコードをレビューするとき
  • クラス、構造体、レコードのどれを選ぶか検討するとき
  • コレクションや列挙可能なオブジェクトを扱うとき

基本原則

  1. 型はシールする - 継承を明示的に想定して設計する場合を除きます
  2. 読み取り専用構造体を優先する - 小さく、変更不可能な値型に適しています
  3. 静的な純粋関数を優先する - パフォーマンスとテスト容易性が向上します
  4. 列挙を遅延させる - 必要になるまで具体化しません
  5. 不変コレクションを返す - API の境界では不変コレクションを返します

構造体とクラスの選択基準

設計時に struct と class のどちらを選ぶかによって、メモリ割り当て、GC の負荷、API の形状に連鎖的な影響が生じます。

判断基準
基準struct が適する場合class が適する場合
サイズ小さい(理想的には 16 バイト以下、許容範囲は 64 バイト以下)大きい、または可変サイズ
寿命短命で、メソッド内に限定される長命で、スコープをまたいで共有される
同一性値の等価性(同じデータを持つ 2 つのインスタンスは等しい)参照の同一性が重要
可変性不変(readonly struct)可変、または複雑な状態遷移がある
継承不要ポリモーフィズムや基底クラスが必要
null 許容の意味default が有効なゼロ状態不在を示すために明示的な null が必要
コレクションでの使用配列やスパンに格納(連続したメモリ)参照を介して格納(間接参照)
サイズの指針
<= 16 bytes:  Ideal struct -- fits in two registers, passed efficiently
17-64 bytes:  Acceptable struct -- measure copy cost vs allocation cost
> 64 bytes:   Prefer class -- copying cost outweighs allocation avoidance
一般的な型と適切な設計
型適切な選択理由
Point2D(8 バイト:2 つの float)readonly struct小さく、不変で、値の意味論を持つ
Money(16 バイト:decimal と通貨)readonly struct小さく、不変で、値の等価性を持つ
DateRange(16 バイト:2 つの DateOnly)readonly struct小さく、不変で、値の意味論を持つ
Matrix4x4(64 バイト:16 個の float)struct(in パラメーターを使用)パフォーマンスが重要な数学処理
CustomerDto(可変:文字列、リスト)class または record参照を含み、サイズが可変
HttpRequest のコンテキストclass長命で、ミドルウェア間で共有される

デフォルトでシールする

ライブラリ型をシールする理由

ライブラリ型(他のアセンブリから利用されるコード)では、クラスをデフォルトでシールします。

  1. JIT の逆仮想化 -- シールクラスを使うと、JIT は仮想呼び出しを直接呼び出しに置き換えられるため、インライン化が可能になります
  2. 契約の簡素化 -- シールされていないクラスは、継承をサポートするという約束を意味します
  3. 破壊的変更の削減 -- 後からクラスをシールすると、バイナリ互換性を破る変更になります
// GOOD -- sealed by default for library types
public sealed class WidgetService
{
    public Widget GetWidget(int id) => new(id, "Default");
}

// Only unseal when inheritance is an intentional design decision
public abstract class WidgetValidatorBase
{
    public abstract bool Validate(Widget widget);
    protected virtual void OnValidationComplete(Widget widget) { }
}
シールしない場合
状況理由
抽象基底クラス継承が目的です
フレームワークの拡張ポイント利用者がサブクラス化する必要があります
モックに対応していない設計でのテスト用ダブルモックフレームワークにはサブクラス化が必要です
アプリケーション内部のクラスシールしても利点がありません

読み取り専用構造体

すべてのフィールドが不変の場合は、構造体に readonly を指定します。これにより、in パラメーターや readonly フィールドを介して構造体にアクセスする際に JIT が生成する防御的コピーをなくせます。

防御的コピーの問題
// NON-readonly struct -- JIT must defensively copy on every method call
public struct MutablePoint
{
    public double X;
    public double Y;
    public double Length() => Math.Sqrt(X * X + Y * Y);
}

public double GetLength(in MutablePoint point)
{
    return point.Length(); // Hidden copy here!
}
// GOOD -- readonly struct: JIT knows no mutation is possible
public readonly struct ImmutablePoint
{
    public double X { get; }
    public double Y { get; }

    public ImmutablePoint(double x, double y) => (X, Y) = (x, y);

    public double Length() => Math.Sqrt(X * X + Y * Y);
}

public double GetLength(in ImmutablePoint point)
{
    return point.Length(); // No copy, direct call
}
読み取り専用構造体のチェックリスト
  • すべてのフィールドが readonly であるか、プロパティが { get; } / { get; init; } である
  • 状態を変更するメソッドがない
  • コンストラクターですべてのフィールドを初期化する
  • ボックス化せずに値を比較するため、IEquatable<T> の実装を検討する

データ転送におけるレコード型

record class と record struct
特徴record classrecord struct
メモリ割り当てヒープスタック(または配列内にインラインで格納)
等価性値の等価性を持つ参照型値の等価性を持つ値型
with 式新しいヒープオブジェクトを作成新しいスタック上のコピーを作成
null 許容不在を null で表す空の状態を default で表す
サイズ参照(x64 では 8 バイト)+ヒープスタック上で完全なサイズ
// record class -- heap allocated, good for DTOs
public record CustomerDto(string Name, string Email, DateOnly JoinDate);

// readonly record struct -- stack allocated, good for small value objects
public readonly record struct Money(decimal Amount, string Currency);

静的な純粋関数を優先する

副作用のない静的メソッドは、より高速でテストしやすくなります。

// DO: Static pure function
public static class OrderCalculator
{
    public static Money CalculateTotal(IReadOnlyList<OrderItem> items)
    {
        var total = items.Sum(i => i.Price * i.Quantity);
        return new Money(total, "USD");
    }
}

// Usage - predictable, testable
var total = OrderCalculator.CalculateTotal(items);

利点:

  • vtable の検索が不要(より高速)
  • 隠れた状態がない
  • テストしやすい(純粋な入力 → 出力)
  • 設計上スレッドセーフ
  • 依存関係の明示を促す

列挙を遅延する

必要になるまで列挙可能なオブジェクトを実体化しないでください。過剰な LINQ チェーンは避けてください。

// BAD: Premature materialization
public IReadOnlyList<Order> GetActiveOrders()
{
    return _orders
        .Where(o => o.IsActive)
        .ToList()  // Materialized!
        .OrderBy(o => o.CreatedAt)  // Another iteration
        .ToList();  // Materialized again!
}

// GOOD: Defer until the end
public IReadOnlyList<Order> GetActiveOrders()
{
    return _orders
        .Where(o => o.IsActive)
        .OrderBy(o => o.CreatedAt)
        .ToList();  // Single materialization
}

// GOOD: Return IEnumerable if caller might not need all items
public IEnumerable<Order> GetActiveOrders()
{
    return _orders
        .Where(o => o.IsActive)
        .OrderBy(o => o.CreatedAt);
}
非同期列挙
// GOOD: Use IAsyncEnumerable for streaming
public async IAsyncEnumerable<OrderResult> ProcessOrdersAsync(
    IEnumerable<Order> orders,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    foreach (var order in orders)
    {
        ct.ThrowIfCancellationRequested();
        yield return await ProcessOrderAsync(order, ct);
    }
}

// GOOD: Batch processing for parallelism
var results = await Task.WhenAll(
    orders.Select(o => ProcessOrderAsync(o)));

ValueTask と Task

同期的に完了することが多いホットパスでは ValueTask を使用してください。実際の I/O には Task を使用してください。

// DO: ValueTask for cached/synchronous paths
public ValueTask<User?> GetUserAsync(UserId id)
{
    if (_cache.TryGetValue(id, out var user))
    {
        return ValueTask.FromResult<User?>(user);  // No allocation
    }

    return new ValueTask<User?>(FetchUserAsync(id));
}

// DO: Task for real I/O (simpler, no footguns)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
    return _repository.CreateAsync(cmd);
}

ValueTask のルール:

  • ValueTask を複数回 await しないでください
  • 完了前に .Result や .GetAwaiter().GetResult() を使用しないでください
  • 迷った場合は Task を使用してください

ref struct と Span/Memory の選択

ref struct の制約

ref struct 型はスタック専用です。ボックス化できず、ref struct 以外の型のフィールドに格納できず、非同期メソッドで使用することもできません。

Span と Memory の選択
基準Span<T> を使用Memory<T> を使用
同期メソッドはいはい(ただし Span のほうがオーバーヘッドが小さい)
非同期メソッドいいえ(ref struct)はい
フィールド/コレクションに格納いいえ(ref struct)はい
コールバック/デリゲートに渡すいいえはい
割り当てなしでスライスするはいはい
stackalloc バッファーをラップするはいいいえ
選択フローチャート
Will the buffer be used in an async method or stored in a field?
  YES -> Use Memory<T> (convert to Span<T> with .Span for synchronous processing)
  NO  -> Do you need to wrap a stackalloc buffer?
           YES -> Use Span<T>
           NO  -> Prefer Span<T> for lowest overhead
実用的なパターン
// Public API uses Memory<T> for maximum flexibility
public async Task<int> ProcessAsync(ReadOnlyMemory<byte> data,
    CancellationToken ct = default)
{
    await _stream.WriteAsync(data, ct);
    return CountNonZero(data.Span);
}

// Internal hot-path method uses Span<T> for zero overhead
private static int CountNonZero(ReadOnlySpan<byte> data)
{
    var count = 0;
    foreach (var b in data)
    {
        if (b != 0) count++;
    }
    return count;
}
Span の一般的なパターン
// Slice without allocation
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
var hello = span[..5];  // No allocation

// Stack allocation for small buffers
Span<byte> buffer = stackalloc byte[256];

// Use ArrayPool for larger buffers
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
    // Use buffer...
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

コレクション型の選択

判断マトリックス
シナリオ推奨型理由
一度構築し、何度も読み取るFrozenDictionary<K,V> / FrozenSet<T>読み取り向けに最適化されたレイアウト(.NET 8 以降)
一度構築し、何度も読み取る(.NET 8 より前)ImmutableDictionary<K,V>スレッドセーフで不変
並行して読み書きするConcurrentDictionary<K,V>外部ロックなしでスレッドセーフ
頻繁に変更するDictionary<K,V>操作ごとのオーバーヘッドが最小
順序付きデータSortedDictionary<K,V>ソートされた列挙で O(log n) の検索
パブリック API から返すIReadOnlyList<T> / IReadOnlyDictionary<K,V>不変のインターフェイス
スタックに割り当てる小規模なコレクションstackalloc を使った Span<T>GC の負荷ゼロ
FrozenDictionary(.NET 8 以降)

FrozenDictionary<K,V> は、最大限の読み取りパフォーマンスを実現するため、作成時に内部レイアウトを最適化します。

using System.Collections.Frozen;

private static readonly FrozenDictionary<string, int> StatusCodes =
    new Dictionary<string, int>
    {
        ["OK"] = 200,
        ["NotFound"] = 404,
        ["InternalServerError"] = 500
    }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);

public int GetStatusCode(string name) =>
    StatusCodes.TryGetValue(name, out var code) ? code : -1;

FrozenDictionary を使用する場合:

  • 起動時に設定値を格納するルックアップテーブル
  • 静的マッピング(enum から文字列への変換、エラーコード、機能フラグ)
  • 一度だけ値を設定し、何度も読み取る辞書

使用しない場合:

  • 実行時に変更されるデータ
  • 最適化のオーバーヘッドを回収できない小規模なルックアップ(10 項目未満)
コレクションの戻り値の型
// DO: Return immutable collection
public IReadOnlyList<Order> GetOrders()
{
    return _orders.ToList();
}

// DO: Use frozen collections for static data
private static readonly FrozenDictionary<string, Handler> _handlers =
    new Dictionary<string, Handler>
    {
        ["create"] = new CreateHandler(),
        ["update"] = new UpdateHandler(),
    }.ToFrozenDictionary();

// DON'T: Return mutable collection
public List<Order> GetOrders()
{
    return _orders;  // Caller can modify!
}

クイックリファレンス

パターン利点
sealed class非仮想化、明確な API
readonly record struct防御的コピーなし、値セマンティクス
静的な純粋関数vtable なし、テスト可能、スレッドセーフ
.ToList() を遅延する実体化を一度にする
ホットパスでの ValueTaskTask の割り当てを回避
バイト列に Span<T> を使用スタック割り当て、コピーなし
IReadOnlyList<T> を返す不変の API 契約
FrozenDictionary静的データの最速の検索

アンチパターン

// DON'T: Unsealed class without reason
public class OrderService { }  // Seal it!

// DON'T: Mutable struct
public struct Point { public int X; public int Y; }  // Make readonly

// DON'T: Instance method that could be static
public int Add(int a, int b) => a + b;  // Make static

// DON'T: Multiple ToList() calls
items.Where(...).ToList().OrderBy(...).ToList();  // One ToList at end

// DON'T: Return List<T> from public API
public List<Order> GetOrders();  // Return IReadOnlyList<T>

// DON'T: ValueTask for always-async operations
public ValueTask<Order> CreateOrderAsync();  // Just use Task

// DON'T: Use `Span<T>` in async methods
public async Task ProcessAsync(Span<byte> data);  // Use Memory<T>

// DON'T: Use `FrozenDictionary` for mutable data
// It has no add/remove APIs

エージェントで注意すべき点

  1. すべての型で安易に class を使わないでください -- struct と class の選択基準を確認してください。
  2. readonly ではない struct を作成しないでください -- 可変 struct は、原因の特定が難しいバグを引き起こします。
  3. 非同期メソッドで Span<T> を使わないでください -- 非同期コードでは Memory<T> を使用してください。
  4. 変更可能なデータに FrozenDictionary を使わないでください -- 追加や削除の API がありません。
  5. 抽象クラスや拡張ポイントとして設計されたクラスを sealed にしないでください。
  6. 測定せずに大きな struct(> 64 bytes)を作成しないでください -- コピーのコストをベンチマークしてください。
  7. ホットパスの静的ルックアップテーブルに Dictionary<K,V> を使わないでください -- FrozenDictionary を使用してください。
  8. 大きな readonly struct の in パラメーターを忘れないでください -- in がないと、struct がコピーされます。

リソース

コメント