---
title: "C# 을 쓰는데 결과는 네이티브다"
description: "Burst 는 IL 을 LLVM 으로 넘겨 네이티브 코드를 뽑습니다. 대신 쓸 수 있는 C# 이 부분집합으로 줄고, 그 제약이 곧 최적화의 조건입니다."
author: "유니얼"
pubDate: 2026-09-10
category: "game-dev/unity/dots"
tags: ["Unity", "DOTS", "최적화"]
canonical: https://blog.unialgames.com/posts/dots-burst/
---
잡 시스템으로 일을 워커에 나눠 줘도, 그 워커가 실행하는 것은 여전히 C# 입니다.
Burst 는 그 지점을 바꿉니다. **같은 C# 코드에서 네이티브 코드를 뽑아냅니다.**

```csharp
[BurstCompile]
struct MoveJob : IJobEntity { ... }
```

이 한 줄이 붙은 잡은 다른 경로로 컴파일됩니다.

## IL 을 LLVM 에 넘긴다

C# 은 컴파일하면 IL 이라는 중간 코드가 됩니다.
그다음이 백엔드마다 갈리는데, Burst 는 여기서 **IL 을 받아 LLVM 으로 넘깁니다.**

LLVM 은 C++ 컴파일러들이 쓰는 그 최적화 엔진입니다.
루프 펼치기, 죽은 코드 제거, 인라이닝, 레지스터 할당 같은 것을 전부 해 줍니다.
**C++ 을 컴파일할 때 받던 최적화를 C# 코드가 그대로 받습니다.**

| | 무엇을 하나 |
| --- | --- |
| Mono | IL 을 런타임에 JIT 으로 기계어로 바꿉니다 |
| IL2CPP | IL 을 C++ 로 바꾼 뒤 플랫폼 컴파일러로 빌드합니다 |
| **Burst** | IL 을 **LLVM 으로** 넘겨 최적화된 네이티브를 뽑습니다. 잡과 시스템에만 적용됩니다 |

<figure class="diagram">
<svg viewBox="0 0 680 250" role="img" aria-label="C# 소스가 IL 을 거쳐 Mono 와 IL2CPP 와 Burst 세 갈래로 갈라져 기계어가 되는 흐름">
<g stroke="var(--border)" fill="var(--bg)">
<rect x="20" y="104" width="90" height="40" rx="4"></rect>
</g>
<text x="38" y="129" font-size="12" fill="var(--text-muted)">C# 소스</text>
<g stroke="var(--border)" fill="none">
<path d="M114 124 L152 124"></path>
<path d="M142 119 L152 124 L142 129"></path>
</g>
<g stroke="var(--border)" fill="var(--bg)">
<rect x="156" y="104" width="60" height="40" rx="4"></rect>
</g>
<text x="176" y="129" font-size="12" fill="var(--text-muted)">IL</text>
<g stroke="var(--border)" fill="none">
<path d="M220 124 L250 60 L268 60"></path>
<path d="M258 55 L268 60 L258 65"></path>
<path d="M220 124 L268 124"></path>
<path d="M258 119 L268 124 L258 129"></path>
</g>
<g stroke="var(--accent)" fill="none">
<path d="M220 124 L250 194 L268 194"></path>
<path d="M258 189 L268 194 L258 199"></path>
</g>
<text x="20" y="30" font-size="13" fill="var(--text-muted)">여기서 세 갈래로 갈라집니다</text>
<g stroke="var(--border)" fill="var(--bg)">
<rect x="272" y="42" width="120" height="36" rx="4"></rect>
<rect x="410" y="42" width="120" height="36" rx="4"></rect>
</g>
<text x="286" y="58" font-size="11" fill="var(--text-muted)">Mono</text>
<text x="286" y="72" font-size="10" fill="var(--text-muted)">런타임에 JIT</text>
<text x="424" y="65" font-size="11" fill="var(--text-muted)">기계어</text>
<g stroke="var(--border)" fill="none">
<path d="M396 60 L406 60"></path>
</g>
<g stroke="var(--border)" fill="var(--bg)">
<rect x="272" y="106" width="120" height="36" rx="4"></rect>
<rect x="410" y="106" width="120" height="36" rx="4"></rect>
</g>
<text x="286" y="122" font-size="11" fill="var(--text-muted)">IL2CPP</text>
<text x="286" y="136" font-size="10" fill="var(--text-muted)">C++ 로 바꾼 뒤</text>
<text x="424" y="129" font-size="11" fill="var(--text-muted)">네이티브</text>
<g stroke="var(--border)" fill="none">
<path d="M396 124 L406 124"></path>
</g>
<g stroke="var(--accent)" fill="var(--accent-soft)">
<rect x="272" y="176" width="120" height="36" rx="4"></rect>
<rect x="410" y="176" width="120" height="36" rx="4"></rect>
</g>
<text x="286" y="192" font-size="11" fill="var(--accent)">Burst</text>
<text x="286" y="206" font-size="10" fill="var(--accent)">LLVM 에 넘김</text>
<text x="424" y="199" font-size="11" fill="var(--accent)">최적화된 네이티브</text>
<g stroke="var(--accent)" fill="none">
<path d="M396 194 L406 194"></path>
</g>
<text x="548" y="60" font-size="11" fill="var(--text-muted)">에디터와 일부 플랫폼</text>
<text x="548" y="124" font-size="11" fill="var(--text-muted)">모바일 콘솔 빌드</text>
<text x="548" y="188" font-size="11" fill="var(--accent)">[BurstCompile] 이</text>
<text x="548" y="204" font-size="11" fill="var(--accent)">붙은 잡과 시스템만</text>
<text x="20" y="238" font-size="12" fill="var(--text-muted)">셋 중 하나를 고르는 것이 아닙니다. 한 프로젝트 안에서 위 둘과 아래 하나가 같이 돕니다.</text>
</svg>
<figcaption>Burst 는 런타임을 갈아 끼우는 것이 아니라 특정 코드만 다른 길로 보내는 것입니다.</figcaption>
</figure>

**Burst 는 런타임을 대체하지 않습니다.** 일반 C# 코드는 그대로 Mono 나 IL2CPP 로 돕니다.
`[BurstCompile]` 이 붙은 자리만 다른 길로 갑니다.
그래서 **한 프로젝트 안에 두 세계가 공존**합니다.

## SIMD 를 알아서 쓴다

Burst 가 파는 것 중 제일 큰 것이 **자동 벡터화**입니다.

요즘 CPU 는 명령 하나로 값 여러 개를 한꺼번에 계산할 수 있습니다.
`float` 넷을 더하는 데 덧셈 명령 네 번이 아니라 한 번이면 됩니다.
이걸 SIMD 라 하고, 손으로 쓰려면 플랫폼별 내장 함수를 알아야 합니다.

<figure class="diagram">
<svg viewBox="0 0 680 200" role="img" aria-label="스칼라는 덧셈 명령을 네 번 내고 벡터는 네 값을 한 명령으로 처리한다는 대비">
<text x="20" y="22" font-size="13" fill="var(--text-muted)">스칼라 · 하나씩</text>
<g stroke="var(--border)" fill="var(--bg)">
<rect x="20" y="32" width="130" height="20" rx="2"></rect>
<rect x="20" y="56" width="130" height="20" rx="2"></rect>
<rect x="20" y="80" width="130" height="20" rx="2"></rect>
<rect x="20" y="104" width="130" height="20" rx="2"></rect>
</g>
<text x="30" y="46" font-size="10" fill="var(--text-muted)">c0 = a0 + b0</text>
<text x="30" y="70" font-size="10" fill="var(--text-muted)">c1 = a1 + b1</text>
<text x="30" y="94" font-size="10" fill="var(--text-muted)">c2 = a2 + b2</text>
<text x="30" y="118" font-size="10" fill="var(--text-muted)">c3 = a3 + b3</text>
<g stroke="var(--warn)" fill="none">
<path d="M156 42 L186 42"></path>
<path d="M156 66 L186 66"></path>
<path d="M156 90 L186 90"></path>
<path d="M156 114 L186 114"></path>
</g>
<text x="192" y="82" font-size="12" fill="var(--warn)">덧셈 명령 4번</text>
<g stroke="var(--border)" fill="none">
<path d="M330 20 L330 180"></path>
</g>
<text x="360" y="22" font-size="13" fill="var(--accent)">벡터 · 넷을 한 번에</text>
<g stroke="var(--accent)" fill="var(--accent-soft)">
<rect x="360" y="32" width="40" height="20"></rect>
<rect x="402" y="32" width="40" height="20"></rect>
<rect x="444" y="32" width="40" height="20"></rect>
<rect x="486" y="32" width="40" height="20"></rect>
</g>
<text x="372" y="46" font-size="10" fill="var(--accent)">a0</text>
<text x="414" y="46" font-size="10" fill="var(--accent)">a1</text>
<text x="456" y="46" font-size="10" fill="var(--accent)">a2</text>
<text x="498" y="46" font-size="10" fill="var(--accent)">a3</text>
<text x="536" y="46" font-size="11" fill="var(--text-muted)">+</text>
<g stroke="var(--accent)" fill="var(--accent-soft)">
<rect x="360" y="60" width="40" height="20"></rect>
<rect x="402" y="60" width="40" height="20"></rect>
<rect x="444" y="60" width="40" height="20"></rect>
<rect x="486" y="60" width="40" height="20"></rect>
</g>
<text x="372" y="74" font-size="10" fill="var(--accent)">b0</text>
<text x="414" y="74" font-size="10" fill="var(--accent)">b1</text>
<text x="456" y="74" font-size="10" fill="var(--accent)">b2</text>
<text x="498" y="74" font-size="10" fill="var(--accent)">b3</text>
<g stroke="var(--accent)" fill="none">
<path d="M443 86 L443 100"></path>
<path d="M438 95 L443 100 L448 95"></path>
</g>
<g stroke="var(--accent)" fill="var(--accent-soft)">
<rect x="360" y="104" width="40" height="20"></rect>
<rect x="402" y="104" width="40" height="20"></rect>
<rect x="444" y="104" width="40" height="20"></rect>
<rect x="486" y="104" width="40" height="20"></rect>
</g>
<text x="372" y="118" font-size="10" fill="var(--accent)">c0</text>
<text x="414" y="118" font-size="10" fill="var(--accent)">c1</text>
<text x="456" y="118" font-size="10" fill="var(--accent)">c2</text>
<text x="498" y="118" font-size="10" fill="var(--accent)">c3</text>
<text x="360" y="146" font-size="12" fill="var(--accent)">덧셈 명령 1번</text>
<text x="20" y="172" font-size="12" fill="var(--text-muted)">계산하는 값의 개수는 같습니다. 명령을 내는 횟수가 다릅니다.</text>
<text x="20" y="190" font-size="12" fill="var(--text-muted)">그래서 계산이 무거운 루프보다 단순한 루프에서 효과가 큽니다.</text>
</svg>
<figcaption>Burst 가 이 변환을 자동으로 합니다. 우리가 벡터 명령을 손으로 쓰지 않습니다.</figcaption>
</figure>

계산하는 순서로 보면 이렇습니다. **탭을 바꾸면 코드가 통째로 바뀝니다.**
위쪽이 우리가 쓴 것이고 아래쪽은 Burst 가 만들어 낸 것입니다.

<x-flow>
<script type="application/json">
{
  "label": "값 넷을 더하는 데 명령을 몇 번 내나",
  "dwell": 1.6,
  "cases": [
    {
      "label": "스칼라 · 우리가 쓴 코드",
      "state": "a = [1, 2, 3, 4] · b = [10, 20, 30, 40]",
      "lines": [
        "c[0] = a[0] + b[0];",
        "c[1] = a[1] + b[1];",
        "c[2] = a[2] + b[2];",
        "c[3] = a[3] + b[3];"
      ],
      "path": [
        { "line": 0, "why": "첫 값을 더합니다. 이 순간 CPU 는 레지스터 한 칸만 씁니다.", "out": "덧셈 명령 1회 · c[0] = 11" },
        { "line": 1, "why": "두 번째로 넘어갑니다. 앞 명령이 끝나야 여기로 옵니다.", "out": "덧셈 명령 2회 · c[1] = 22" },
        { "line": 2, "why": "세 번째. 하는 일은 똑같은데 명령을 또 냅니다.", "out": "덧셈 명령 3회 · c[2] = 33" },
        { "line": 3, "why": "네 번째. 값 넷을 더하는 데 덧셈 명령을 넷 썼습니다.", "out": "덧셈 명령 4회 · c[3] = 44" }
      ],
      "note": "네 줄을 네 번에 나눠 밟습니다. 한 칸씩 순서대로입니다."
    },
    {
      "label": "벡터 · Burst 가 바꾼 것",
      "state": "a = [1, 2, 3, 4] · b = [10, 20, 30, 40]",
      "lines": [
        "A = load(a[0..3]);",
        "B = load(b[0..3]);",
        "C = A + B;",
        "store(c[0..3], C);"
      ],
      "path": [
        { "line": 0, "why": "a 의 네 값을 레지스터 한 칸에 나란히 담습니다. 메모리에 붙어 있어서 읽기도 한 번입니다.", "out": "A = [1, 2, 3, 4]" },
        { "line": 1, "why": "b 도 마찬가지로 한 번에 담습니다.", "out": "B = [10, 20, 30, 40]" },
        { "line": 2, "why": "여기가 덧셈입니다. 네 칸이 동시에 더해집니다. 0번이 먼저고 3번이 나중인 것이 아닙니다.", "out": "덧셈 명령 1회 · C = [11, 22, 33, 44]" },
        { "line": 3, "why": "결과 넷을 한 번에 씁니다.", "out": "c = [11, 22, 33, 44]" }
      ],
      "note": "네 번 멈추지만 덧셈은 세 번째 한 번뿐입니다. 나머지는 읽고 쓰는 것입니다."
    }
  ]
}
</script>
</x-flow>

두 번째 탭에서 **읽기와 쓰기도 한 번씩**인 것을 보세요.
`a[0]` 부터 `a[3]` 이 메모리에 붙어 있어서 한 명령으로 퍼 올 수 있었습니다.
흩어져 있었다면 네 번 읽어야 하고, 그러면 덧셈만 묶어도 남는 게 없습니다.
아래 조건 셋 중 첫 번째가 여기서 나옵니다.

Burst 는 루프를 보고 **이걸 자동으로 만들어 줍니다.** 단 조건이 있습니다.

- **데이터가 연속으로 붙어 있어야 합니다.** 포인터를 따라 뛰어다니면 못 합니다
- **반복끼리 서로 의존하지 않아야 합니다.** 앞 결과를 뒤가 쓰면 못 합니다
- **분기가 적어야 합니다.** `if` 가 많으면 벡터로 못 묶습니다

조건이 깨지는 것이 어떤 모양인지는 코드 두 개를 나란히 놓으면 바로 보입니다.

<x-compare>
<script type="application/json">
{
  "tabs": [
    { "label": "벡터화된다", "code": "for (int i = 0; i < n; i++)\n    c[i] = a[i] + b[i];", "text": "i 번째 계산이 다른 반복의 결과를 안 봅니다. 데이터도 연속이라 넷씩 묶어서 한 명령으로 내보낼 수 있습니다.", "stats": [["반복 간 의존", "없음"], ["묶을 수 있나", "가능"], ["결과", "SIMD 명령"]], "tone": "good" },
    { "label": "벡터화 안 된다", "code": "for (int i = 1; i < n; i++)\n    c[i] = c[i - 1] + a[i];", "text": "i 번째가 i-1 번째 결과를 씁니다. 앞이 끝나야 뒤를 계산할 수 있으니 묶을 방법이 없습니다. 줄 수는 거의 같은데 성질이 다릅니다.", "stats": [["반복 간 의존", "있음"], ["묶을 수 있나", "불가능"], ["결과", "스칼라 그대로"]], "tone": "bad" }
  ]
}
</script>
</x-compare>

**ECS 가 데이터를 청크에 연속으로 두는 것이 이 조건을 만들어 줍니다.**
그래서 ECS, 잡 시스템, Burst 셋이 같이 다닙니다.
따로 쓸 수도 있지만 셋이 모였을 때 효과가 곱해집니다.

`Unity.Mathematics` 의 `float3`, `math.length` 같은 것들은
Burst 가 SIMD 로 바꾸기 좋게 만들어진 타입입니다.
`Vector3` 대신 `float3` 을 쓰라는 이유가 이것입니다.

## 쓸 수 있는 C# 이 줄어든다

Burst 가 다루는 것은 C# 전체가 아니라 **HPC#** 이라 부르는 부분집합입니다.
관리 힙에 손대는 것이 전부 빠집니다.

| 되는 것 | 안 되는 것 |
| --- | --- |
| `int` `float` `bool` `double` 같은 기본형 | **`class`** 와 모든 참조 타입 |
| `struct` | **`string`** |
| `float2` `float3` `float4` | **관리 배열** (`int[]`). `NativeArray<T>` 를 씁니다 |
| `enum`, 포인터 | `try`/`catch` 로 잡는 **예외 처리** |
| struct 로 제한한 제네릭 | 박싱이 일어나는 모든 것 |

처음에는 불편한 제약처럼 보이는데, 사실은 **원인과 결과가 뒤집혀 있습니다.**

참조 타입을 쓰면 데이터가 힙 여기저기로 흩어지고,
흩어지면 SIMD 도 안 되고 캐시도 안 맞습니다.
**빠른 코드가 되려면 어차피 못 쓰는 것들**을 컴파일러가 미리 막는 것에 가깝습니다.

> `Debug.Log` 도 안 됩니다. 문자열이 관리 타입이니까요.
> 잡 안을 들여다보려면 `Run()` 으로 바꿔서 Burst 를 끄고 봐야 하는데,
> 그러면 성능이 전혀 다른 코드가 됩니다. **디버깅이 번거로운 게 가장 큰 실무 비용입니다.**

## 언제 컴파일되나

| | 언제 |
| --- | --- |
| **에디터** | JIT. 그 잡을 **처음 실행할 때** 컴파일합니다 |
| **빌드** | AOT. 빌드 시점에 미리 컴파일해 둡니다 |

에디터에서 첫 프레임이 유독 튀는 것이 이것 때문인 경우가 많습니다.
**측정할 때 워밍업 프레임을 버려야 하는 이유가 여기에도 있습니다.**

그리고 에디터에는 Burst 를 끄는 스위치가 있습니다
(`Jobs > Burst > Enable Compilation`).
켜고 끄고 재 보면 Burst 가 실제로 얼마를 벌어 주는지 바로 나옵니다.
**끄고 쟀는데 별 차이가 없으면 그 잡은 애초에 병목이 아니었던 것입니다.**

## 정말 벡터화됐는지 보는 법

`Jobs > Burst > Open Inspector` 를 열면 잡마다 **생성된 어셈블리**가 보입니다.
여기서 확인할 것은 둘입니다.

- **SIMD 명령이 실제로 나왔는가.** `addps`, `vmulps` 처럼 벡터 명령이 보이면 됐습니다
- **경고가 있는가.** 벡터화를 못 한 이유를 적어 주기도 합니다

`[BurstCompile]` 을 붙였다고 벡터화가 보장되지는 않습니다.
붙였는데 안 빨라졌다면 대개 **위의 세 조건 중 하나가 깨진 것**입니다.

## 부동소수 정밀도를 낮출 수 있다

```csharp
[BurstCompile(FloatMode = FloatMode.Fast, FloatPrecision = FloatPrecision.Low)]
```

`FloatMode.Fast` 는 부동소수 연산의 순서를 바꿔도 된다고 허락하는 것입니다.
`(a + b) + c` 를 `a + (b + c)` 로 바꾸면 결과가 미세하게 달라지는데,
그걸 허용하면 최적화 여지가 늘어납니다.

게임 로직에서는 대개 문제가 없습니다.
**단 결정론이 필요하면 쓰면 안 됩니다.** 리플레이나 락스텝 네트워킹처럼
같은 입력이 같은 결과를 내야 하는 곳입니다.

## 조용히 실패한다

가장 조심할 점입니다.

`[BurstCompile]` 을 붙였는데 코드에 관리 타입이 섞여 있으면
**컴파일에 실패하고 원래 C# 경로로 돌아갑니다.**
게임은 정상으로 돌고, 결과도 맞고, **그냥 안 빨라집니다.**

콘솔에 경고가 뜨지만 다른 로그에 묻히기 쉽습니다.
그래서 확인은 셋 중 하나로 합니다.
Burst Inspector 에서 어셈블리가 나오는지 보거나,
프로파일러에서 마커가 `Burst Jobs` 카테고리에 잡히는지 보거나,
Burst 를 껐다 켰을 때 숫자가 움직이는지 보는 것입니다.

**빌드가 통과했다는 것이 Burst 가 걸렸다는 뜻은 아닙니다.**