Optimizing Unity DOTS Memory Layout for 10k Entities

Game Development
Date:November 14, 2026
Topic:
Optimizing Unity DOTS Memory Layout for 10k Entities
⏱ 2 min read

You shipped 10,000 entities. The profiler shows 18ms frame time. The culprit isn't your logic — it's how you laid out memory. Unity DOTS promises performance, but only if you stop treating ECS like OOP with structs.

Why Memory Layout Breaks Your Frame Budget

ECS processes data in chunks. Each archetype gets its own contiguous memory block. When you scatter unrelated components across entities, you force the CPU to jump between cache lines. Burst compiles vectorized code, but it can't vectorize what isn't contiguous.

"

The fastest code is code that never misses L1 cache.

— Mike Acton, former Unity DOTS lead

Design for Archetype Stability

Every AddComponent/RemoveComponent call may move entities between archetypes. At 10k entities, that's 10k potential memory copies per frame. Instead, use enableable components or state tags.

csharp
// Bad: structural changes every frame
if (entity.IsDead) {
    ecb.RemoveComponent<Health>(entity);
    ecb.AddComponent<DeadTag>(entity);
}

// Good: enableable component, zero archetype churn
[EnableableComponent]
public struct Health : IComponentData {
    public float Value;
}

// System
foreach (var health in SystemAPI.Query<RefRW<EnabledRefRW<Health>>>()) {
    if (health.ValueRO.Value <= 0) {
        health.ValueRW.Value = false; // Disables component, same archetype
    }
}
💡
TipEnableable components keep entities in the same archetype. Burst sees identical memory layout every frame.

Pack Components by Access Pattern

Components accessed together should live in the same struct. The Burst compiler auto-vectorizes loops over IComponentData arrays — but only when fields are adjacent in memory.

csharp
// Scattered: cache misses on every field
public struct Position : IComponentData { public float3 Value; }
public struct Velocity : IComponentData { public float3 Value; }
public struct Rotation : IComponentData { public quaternion Value; }

// Packed: single cache line loads all three
[InternalBufferCapacity(0)]
public struct Motion : IComponentData {
    public float3 Position;
    public float3 Velocity;
    public quaternion Rotation;
}

Use ISharedComponentData for Rarely Changing Data

Material indices, layer masks, or team IDs change once per entity lifetime. Put them in ISharedComponentData. Unity groups entities by shared component value, creating natural batching for rendering and broadphase.

Component TypeChanges/FrameBest Practice
Transform10,000IComponentData (packed)
Health500Enableable IComponentData
MaterialID0ISharedComponentData
TeamID0ISharedComponentData

Profile With Burst Inspector, Not Profiler Alone

The Unity Profiler shows managed overhead. Burst Inspector shows assembly. Look for 'ldr'/'str' instruction density — high density means cache-friendly. Low density with many 'bl' calls means function call overhead from fragmented memory.

⚠️
WarningDon't guess. Open Burst Inspector → Jobs → YourSystem → Check 'Show Assembly'. Count cache misses per iteration.

Your 2026 Checklist Before Shipping

  1. Audit every structural change — replace with enableable components or tags
  2. Group co-accessed fields into single IComponentData structs
  3. Move immutable data to ISharedComponentData
  4. Run Burst Inspector on hot systems — verify vectorized loops
  5. Test on target hardware: Steam Deck, mobile, low-end desktop

✦

Memory layout isn't optimization — it's correctness. Get it right, and 10k entities run at 60fps on integrated graphics. Get it wrong, and no amount of Burst attributes saves you.

Share𝕏 Twitterin LinkedInin Whatsapp