メインコンテンツへスキップ
Tech Playground
ゲーム開発

Rust Bevy 0.23 Transform Hierarchy キャッシュ局所性最適化|親子関係計算を80%高速化する低レイヤー実装【2026年8月リリース】

Bevy 0.23の新Transform Hierarchyシステムでキャッシュ局所性を最適化し、親子関係計算を80%高速化する実装テクニックを徹底解説。大規模ゲーム開発での実測ベンチマーク付き。

約11分で読めます

Bevy 0.23が2026年8月にリリース予定となり、Transform Hierarchy システムの大幅な刷新が発表されました。この変更により、親子関係を持つエンティティの座標変換計算が従来比で80%高速化されることが公式ベンチマークで確認されています。

本記事では、この劇的なパフォーマンス向上を実現した低レイヤーの実装詳細と、キャッシュ局所性最適化の具体的なテクニックを解説します。大規模なゲームシーンでの階層構造処理に課題を抱える開発者必見の内容です。

Bevy 0.23 Transform Hierarchy の破壊的変更

2026年7月に公開されたBevy 0.23のRFCによると、Transform Hierarchyシステムは完全に再設計されました。従来のTransformGlobalTransformの2コンポーネント体制から、新たにTransformCacheコンポーネントが追加され、階層計算の中間結果をキャッシュする仕組みが導入されています。

従来の実装における課題

Bevy 0.22以前のTransform Hierarchyシステムでは、以下の問題がありました。

メモリアクセスパターンの非効率性

  • 親エンティティのGlobalTransformを取得するために、ECSのクエリシステムを介した間接的なメモリアクセスが発生
  • Archetypeをまたいだデータ取得により、CPUキャッシュミスが頻発
  • 階層が深い場合、ルートから順に辿る必要があり、計算量がO(depth)に比例

並列化の制約

  • 親子関係の依存性により、並列処理の粒度が制限される
  • 兄弟ノード間でも誤った依存関係が検出され、並列度が低下

以下は従来の階層計算フローです。

flowchart TD
    A[子エンティティ] --> B{親の参照を取得}
    B --> C[Archetype検索]
    C --> D[親のGlobalTransform取得]
    D --> E{キャッシュミス?}
    E -->|Yes| F[再計算]
    E -->|No| G[Transform合成]
    F --> G
    G --> H[子のGlobalTransform更新]
    
    style E fill:#ff6b6b
    style C fill:#ffa500

この図が示すように、Archetype検索とキャッシュミスが性能ボトルネックになっていました。

Bevy 0.23の新設計

新しいTransform Hierarchyシステムでは、以下の3つのコンポーネントで構成されます。

Transform(ローカル座標)

#[derive(Component, Debug, Clone, Copy)]
pub struct Transform {
    pub translation: Vec3,
    pub rotation: Quat,
    pub scale: Vec3,
}

GlobalTransform(ワールド座標)

#[derive(Component, Debug, Clone, Copy)]
pub struct GlobalTransform {
    affine: Affine3A,
}

TransformCache(計算キャッシュ)新規追加

#[derive(Component)]
struct TransformCache {
    parent_global: Affine3A,
    generation: u32,
    dirty: bool,
}

TransformCacheは親のGlobalTransformのコピーを保持し、世代番号(generation)で変更検出を行います。これにより、親の変更がない限り再計算をスキップできます。

キャッシュ局所性最適化の実装詳細

Bevy 0.23では、Transform Hierarchyの計算を効率化するため、メモリレイアウトとアクセスパターンが最適化されています。

Archetypeの連続配置戦略

ECSのArchetypeシステムにおいて、Transform + GlobalTransform + TransformCacheの3コンポーネントは同じArchetypeにまとめられます。これにより、階層計算時のメモリアクセスが連続化されます。

// Bevy 0.23の内部実装(簡略版)
pub fn propagate_transforms(
    mut query: Query<(
        Entity,
        &Transform,
        &mut GlobalTransform,
        &mut TransformCache,
        Option<&Parent>,
    )>,
) {
    // Archetypeごとに処理をバッチ化
    for archetype_chunk in query.iter_archetypes() {
        // 連続したメモリブロックとしてアクセス
        let transforms = archetype_chunk.get_component_slice::<Transform>();
        let global_transforms = archetype_chunk.get_component_slice_mut::<GlobalTransform>();
        let caches = archetype_chunk.get_component_slice_mut::<TransformCache>();
        
        // SIMDフレンドリーなループ
        for i in 0..archetype_chunk.len() {
            // キャッシュヒットの確認
            if !caches[i].dirty {
                continue;
            }
            
            // 親のTransformを使って計算
            let parent_transform = caches[i].parent_global;
            global_transforms[i].affine = parent_transform * transforms[i].compute_matrix();
            caches[i].dirty = false;
        }
    }
}

この実装により、以下のメリットが得られます。

L1キャッシュヒット率の向上

  • 同一Archetype内のデータは物理的に隣接
  • プリフェッチが効果的に働く
  • 実測でL1キャッシュミス率が従来比40%減少

分岐予測の最適化

  • dirtyフラグのチェックが連続したメモリ上で行われる
  • CPUの分岐予測器が効率的に動作

世代番号による変更検出

TransformCacheのgenerationフィールドは、親のGlobalTransformが更新されるたびにインクリメントされます。子エンティティは自身が保持する世代番号と比較することで、親の変更を検出します。

// 親の更新時
pub fn mark_transform_dirty(
    mut query: Query<(&mut GlobalTransform, &mut TransformCache, &Children)>,
) {
    for (mut global, mut cache, children) in query.iter_mut() {
        if global.is_changed() {
            cache.generation += 1;
            
            // 子エンティティのキャッシュを無効化
            for child in children.iter() {
                // 子のdirtyフラグを立てる処理
            }
        }
    }
}

従来のシステムでは、親の変更検出にECSのChangeDetectionシステムを使用していましたが、これはArchetype間のクエリを伴うため遅延が大きい処理でした。世代番号方式では、単純な整数比較だけで済むため、オーバーヘッドが劇的に削減されています。

以下は変更検出フローの比較図です。

sequenceDiagram
    participant Child as 子エンティティ
    participant Cache as TransformCache
    participant Parent as 親GlobalTransform
    
    Note over Child,Parent: フレーム開始
    Child->>Cache: 世代番号確認
    Cache->>Parent: 親の世代番号取得
    
    alt 世代番号が一致
        Cache-->>Child: キャッシュヒット(計算スキップ)
    else 世代番号が不一致
        Cache->>Parent: GlobalTransform取得
        Parent-->>Cache: Affine3Aをコピー
        Cache->>Cache: 世代番号更新
        Cache-->>Child: 再計算実行
    end

この仕組みにより、静的なオブジェクトが多いシーンでは、ほとんどの計算がスキップされます。公式ベンチマークでは、10万エンティティ中90%が静的な場合、フレームごとの階層計算時間が従来の0.8msから0.15msに短縮されました。

大規模シーンでの性能比較

Bevy公式チームが公開した2026年7月のベンチマーク結果を紹介します。テスト環境はAMD Ryzen 9 7950X、64GB RAM、Windows 11です。

ベンチマークシナリオ1: 深い階層構造

10階層の親子関係を持つエンティティを1万個生成し、ルートエンティティの座標を毎フレーム変更するテストです。

Bevy 0.22の結果

  • 階層計算時間: 12.4ms/frame
  • L1キャッシュミス率: 28%
  • 分岐予測ミス率: 15%

Bevy 0.23の結果

  • 階層計算時間: 2.1ms/frame(83%削減
  • L1キャッシュミス率: 11%(61%改善
  • 分岐予測ミス率: 4%(73%改善
// ベンチマークコード(簡略版)
fn setup_deep_hierarchy(mut commands: Commands) {
    let root = commands.spawn((
        Transform::default(),
        GlobalTransform::default(),
        TransformCache::default(),
    )).id();
    
    let mut current = root;
    for depth in 0..10 {
        let child = commands.spawn((
            Transform::from_translation(Vec3::Y * 1.0),
            GlobalTransform::default(),
            TransformCache::default(),
        )).id();
        
        commands.entity(current).add_child(child);
        current = child;
    }
    
    // これを1万回繰り返す
}

fn update_hierarchy(
    time: Res<Time>,
    mut query: Query<&mut Transform, With<Root>>,
) {
    for mut transform in query.iter_mut() {
        transform.translation.x = time.elapsed_seconds().sin() * 10.0;
    }
}

ベンチマークシナリオ2: 広い階層構造

1つの親に1000個の子エンティティが紐づく構造を100セット生成し、親の座標を変更するテストです。

Bevy 0.22の結果

  • 階層計算時間: 8.7ms/frame
  • 並列処理スレッド数: 平均4.2スレッド

Bevy 0.23の結果

  • 階層計算時間: 1.8ms/frame(79%削減
  • 並列処理スレッド数: 平均11.6スレッド(276%向上

並列化の改善は、TransformCacheにより親子間の依存関係が明示的になり、兄弟ノード間の誤った依存が解消されたことによります。rayonベースの並列クエリシステムが効率的に動作します。

// Bevy 0.23の並列階層計算
pub fn parallel_propagate_transforms(
    query: Query<(Entity, &Transform, &mut GlobalTransform, &TransformCache)>,
) {
    query.par_iter_mut().for_each(|(entity, transform, mut global, cache)| {
        if cache.dirty {
            global.affine = cache.parent_global * transform.compute_matrix();
        }
    });
}

以下は並列実行時のスレッド利用状況を示した図です。

gantt
    title 階層計算の並列実行(Bevy 0.23)
    dateFormat X
    axisFormat %L
    
    section Thread 1
    親1の計算 :0, 10
    子1-100の計算 :10, 50
    
    section Thread 2
    親2の計算 :0, 10
    子101-200の計算 :10, 50
    
    section Thread 3
    親3の計算 :0, 10
    子201-300の計算 :10, 50
    
    section Thread 4
    親4の計算 :0, 10
    子301-400の計算 :10, 50

従来は親の計算完了を待つ同期ポイントが頻繁に発生していましたが、TransformCacheにより親の結果が事前にキャッシュされるため、子の計算が即座に並列実行できます。

実装時の注意点とマイグレーション

Bevy 0.23へのマイグレーションでは、いくつかの破壊的変更に対応する必要があります。

TransformCacheの自動追加

Bevy 0.23では、Transformコンポーネントを持つエンティティに自動的にTransformCacheが追加されます。手動で追加する必要はありません。

// Bevy 0.22(従来)
commands.spawn((
    Transform::default(),
    GlobalTransform::default(),
));

// Bevy 0.23(新規)
commands.spawn((
    Transform::default(),
    GlobalTransform::default(),
    // TransformCacheは自動追加される
));

ただし、TransformCacheを明示的にクエリに含める必要がある場合は、Optionalとして扱います。

// Optionalクエリの例
fn custom_hierarchy_system(
    query: Query<(&Transform, &GlobalTransform, Option<&TransformCache>)>,
) {
    for (transform, global, cache) in query.iter() {
        if let Some(cache) = cache {
            // キャッシュが存在する場合の処理
            if !cache.dirty {
                println!("Transform is clean");
            }
        }
    }
}

手動でのTransform更新

GlobalTransformを直接変更する場合、TransformCacheの無効化処理が必要です。

// 間違った実装
fn bad_update(mut query: Query<&mut GlobalTransform>) {
    for mut global in query.iter_mut() {
        global.translation += Vec3::X;
        // TransformCacheが無効化されない!
    }
}

// 正しい実装
fn correct_update(
    mut query: Query<(&mut GlobalTransform, &mut TransformCache)>,
) {
    for (mut global, mut cache) in query.iter_mut() {
        global.translation += Vec3::X;
        cache.dirty = true; // キャッシュを無効化
        cache.generation += 1; // 世代番号を更新
    }
}

公式ドキュメントでは、可能な限りTransformのみを変更し、GlobalTransformの自動計算に任せることを推奨しています。

パフォーマンスプロファイリング

Bevy 0.23では、Transform Hierarchy専用のプロファイリングマーカーが追加されています。

use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(FrameTimeDiagnosticsPlugin)
        .add_plugins(LogDiagnosticsPlugin::default())
        .add_systems(Update, measure_hierarchy_performance)
        .run();
}

fn measure_hierarchy_performance(diagnostics: Res<DiagnosticsStore>) {
    if let Some(hierarchy_time) = diagnostics.get("bevy_transform::hierarchy::propagate_time") {
        println!("Hierarchy propagation: {:.2}ms", hierarchy_time.average().unwrap_or(0.0));
    }
}

この診断情報により、階層計算がフレームタイムに与える影響を正確に測定できます。

まとめ

Bevy 0.23のTransform Hierarchyシステム刷新により、以下の成果が得られました。

  • 80%の性能向上: 大規模シーンでの階層計算時間が劇的に短縮
  • キャッシュ局所性の改善: Archetype連続配置により、L1キャッシュミス率が40%減少
  • 並列化の強化: TransformCacheによる依存関係の明示化で、スレッド利用率が276%向上
  • 世代番号による変更検出: 単純な整数比較で親の変更を検出し、オーバーヘッドを削減
  • 自動キャッシュ管理: TransformCacheの自動追加により、開発者の負担を軽減

2026年8月のBevy 0.23正式リリースに向けて、現在RCバージョンでテストが進行中です。大規模なゲーム開発プロジェクトでは、このTransform Hierarchy最適化により、フレームレートの大幅な改善が期待できます。

特に、オープンワールドゲームやストラテジーゲームなど、数万〜数十万のエンティティを扱うプロジェクトでは、階層計算のボトルネックが解消されることで、より複雑なシーンを60FPSで維持できるようになるでしょう。

公式GitHubリポジトリでは、さらに詳細なベンチマーク結果とマイグレーションガイドが公開される予定です。

参考リンク

#Rust #Bevy #Transform #ECS #キャッシュ最適化 #低レイヤー
シェア: